Abhijit Sarkar
Abhijit Sarkar

Reputation: 24617

How to find commit distance using JGit?

I've a little Kotlin utility class that uses JGit to find the following information:

branch, latestCommit, lastTag, lastTagCommit, lastReleaseTag, lastReleaseTagCommit, commitDistance

where lastReleaseTag is found by matching a given prefix.

All that is working, except for commitDistance, which is the number of commits between the latestCommit and a tag. I'm using RevWalkUtils.count, but it always returns zero.

class GitRepo(dir: File) {
    private val log = LoggerFactory.getLogger(GitRepo::class.java)

    constructor(dir: String) : this(File(dir))

    private val repository = FileRepositoryBuilder()
        .setGitDir(File(dir, ".git"))
        .readEnvironment()
        .findGitDir()
        .setMustExist(true)
        .build()

    @JvmOverloads
    fun info(releaseTagPrefix: String = "release/"): RepoInfo {
        repository.use { repo ->
            RevWalk(repo).use { walk ->
                val latestCommit: RevCommit? = Git(repository).use {
                    try {
                        it.log().setMaxCount(1).call().iterator().next()
                    } catch (ex: NoHeadException) {
                        log.warn("Repository has no HEAD")
                        null
                    }
                }

                val tags = repo.refDatabase.getRefsByPrefix("refs/tags/")
                    .groupBy { it.name.startsWith("refs/tags/$releaseTagPrefix") }
                    .mapValues { entry ->
                        entry.value.maxByOrNull { it.name }
                    }
                val lastReleaseTag = tags[true]
                val lastTag = tags[false]
                val lastTagCommit = lastTag?.toRevCommit(walk)
                val commitDistance = if (latestCommit == null || lastTagCommit == null) 0
                else RevWalkUtils.count(walk, latestCommit, lastTagCommit)

                return RepoInfo(
                    repo.branch,
                    latestCommit?.toObjectId()?.shorten(),
                    lastTag?.tagName(),
                    lastTag?.objectId?.shorten(),
                    lastReleaseTag?.tagName(),
                    lastReleaseTag?.objectId?.shorten(),
                    commitDistance
                )
            }
        }
    }

    private fun ObjectId.shorten(): String {
        return name.take(8)
    }

    private fun Ref.tagName(): String? {
        return "refs\\/tags\\/(.*)".toRegex().find(this.name)?.groupValues?.get(1)
    }

    private fun Ref.toRevCommit(revWalk: RevWalk): RevCommit? {
        val id = repository.refDatabase.peel(this)?.peeledObjectId ?: objectId
        return try {
            revWalk.parseCommit(id)
        } catch (ex: MissingObjectException) {
            log.warn("Tag: {} points to a non-existing commit", tagName())
            null
        }
    }
}

A command line invocation of git rev-list --count start...end returns 33.

JGit 5.9.0.202009080501-r.

Upvotes: 1

Views: 397

Answers (1)

Abhijit Sarkar
Abhijit Sarkar

Reputation: 24617

Thanks to @fredrik, it's just a simple matter of swapping the commits in the call to RevWalkUtils.count. However, it turns out that RevWalkUtils.count is returning a greater number than git rev-list --count start...end, perhaps because of this:

count the number of commits that are reachable from start until a commit that is reachable from end is encountered

I ended up changing my implementation as follows:

class GitRepo(dir: File) {
    constructor(dir: String) : this(File(dir))

    private val log = LoggerFactory.getLogger(GitRepo::class.java)
    private val repository = FileRepositoryBuilder()
        .setGitDir(File(dir, ".git"))
        .readEnvironment()
        .findGitDir()
        .setMustExist(true)
        .build()

    @JvmOverloads
    fun info(tagPrefix: String = ".*"): RepoInfo {
        repository.use { repo ->
            val lastTag: Ref? = repo.refDatabase.getRefsByPrefix("refs/tags/")
                .filter { it.name.matches("refs/tags/$tagPrefix".toRegex()) }
                .maxByOrNull { it.name }
            var latestCommit: RevCommit? = null
            var lastTagCommit: RevCommit?
            var commitDistance = 0
            Git(repo).use { git ->
                try {
                    latestCommit = git.log().setMaxCount(1).call().firstOrNull()
                    lastTagCommit = lastTag?.let {
                        val id = repo.refDatabase.peel(it)?.peeledObjectId ?: it.objectId
                        git.log().add(id).call().firstOrNull()
                    }
                    if (latestCommit != null && lastTagCommit != null) {
                        commitDistance = git.log().addRange(lastTagCommit, latestCommit).call().count()
                    }
                } catch (ex: NoHeadException) {
                    log.warn("Repository has no HEAD")
                } catch (ex: MissingObjectException) {
                    log.warn("Tag: {} points to a non-existing commit: ", lastTag?.tagName(), ex.objectId.shorten())
                }

                return RepoInfo(
                    repo.branch,
                    latestCommit?.toObjectId()?.shorten(),
                    lastTag?.tagName(),
                    lastTag?.objectId?.shorten(),
                    commitDistance
                )
            }
        }
    }

    private fun ObjectId.shorten(): String {
        return name.take(8)
    }

    private fun Ref.tagName(): String? {
        return "refs\\/tags\\/(.*)".toRegex().find(name)?.groupValues?.get(1)
    }
}

Upvotes: 2

Related Questions