diff --git a/src/main/kotlin/app/hashers/CodeLongevity.kt b/src/main/kotlin/app/hashers/CodeLongevity.kt index 88d5e91b..96edf6c3 100644 --- a/src/main/kotlin/app/hashers/CodeLongevity.kt +++ b/src/main/kotlin/app/hashers/CodeLongevity.kt @@ -17,6 +17,7 @@ import org.eclipse.jgit.diff.DiffFormatter import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.RawText import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.AnyObjectId import org.eclipse.jgit.lib.Repository import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevWalk @@ -29,15 +30,21 @@ import java.util.Date /** * Represents a code line in a file revision. */ -class RevCommitLine(val commit: RevCommit, val file: String, val line: Int) +class RevCommitLine(val commit: RevCommit, val fileId: AnyObjectId, + val file: String, val line: Int, + val isDeleted: Boolean) { + + val id : String + get() = "${fileId.getName()}:$line" +} /** * Represents a code line in repo's history. * * TODO(Alex): the text arg is solely for testing proposes (remove it) */ -class CodeLine(val from: RevCommitLine, val to: RevCommitLine, - val text: String) { +class CodeLine(val repo: Repository, + val from: RevCommitLine, val to: RevCommitLine) { // TODO(alex): oldId and newId may be computed as a hash built from commit, // file name and line number, if we are going to send the data outside a @@ -48,18 +55,39 @@ class CodeLine(val from: RevCommitLine, val to: RevCommitLine, * identify a line and update its lifetime computed at the previous * iteration. */ - val oldId: String = "" + val oldId : String + get() = from.id /** * Id of the code line in a revision, where the line was deleted, or a head * revision, if the line is alive. */ - val newId: String = "" + val newId : String + get() = to.id /** * The code line's age in seconds. */ - val age = to.commit.getCommitTime() - from.commit.getCommitTime() + val age : Long + get() = (to.commit.getCommitTime() - from.commit.getCommitTime()).toLong() + + /** + * The code line text. + */ + val text : String + get() = RawText(repo.open(from.fileId).getBytes()).getString(from.line) + + /** + * Email address of the line's author. + */ + val email : String + get() = to.commit.authorIdent.emailAddress + + /** + * True if the line is deleted. + */ + val isDeleted : Boolean + get() = to.isDeleted /** * A pretty print of a code line; debugging. @@ -70,8 +98,9 @@ class CodeLine(val from: RevCommitLine, val to: RevCommitLine, val td = df.format(Date(to.commit.getCommitTime().toLong() * 1000)) val fc = "${from.commit.getName()} '${from.commit.getShortMessage()}'" val tc = "${to.commit.getName()} '${to.commit.getShortMessage()}'" + val state = if (isDeleted) "deleted in" else "last known as" return "Line '$text' - '${from.file}:${from.line}' added in $fc $fd\n" + - " last known as '${to.file}:${to.line}' in $tc $td" + " ${state} '${to.file}:${to.line}' in $tc $td" } } @@ -81,13 +110,10 @@ class CodeLine(val from: RevCommitLine, val to: RevCommitLine, class CodeLongevity(private val localRepo: LocalRepo, private val serverRepo: Repo, private val api: Api, - private val git: Git, tailRev: String = "") { + private val git: Git) { val repo: Repository = git.repository val head: RevCommit = RevWalk(repo).parseCommit(repo.resolve(RepoHelper.MASTER_BRANCH)) - val tail: RevCommit? = - if (tailRev != "") RevWalk(repo).parseCommit(repo.resolve(tailRev)) - else null val df = DiffFormatter(DisabledOutputStream.INSTANCE) init { @@ -154,9 +180,9 @@ class CodeLongevity(private val localRepo: LocalRepo, * Returns a list of code lines, both alive and deleted, between * the revisions of the repo. */ - fun getLinesList() : List { + fun getLinesList(tail : RevCommit? = null) : List { val codeLines: MutableList = mutableListOf() - getLinesObservable().blockingSubscribe { line -> + getLinesObservable(tail).blockingSubscribe { line -> codeLines.add(line) } return codeLines @@ -166,30 +192,31 @@ class CodeLongevity(private val localRepo: LocalRepo, * Returns an observable for for code lines, both alive and deleted, between * the revisions of the repo. */ - private fun getLinesObservable(): Observable = + fun getLinesObservable(tail : RevCommit? = null) : Observable = Observable.create { subscriber -> - val treeWalk = TreeWalk(repo) - treeWalk.setRecursive(true) - treeWalk.addTree(head.getTree()) + val headWalk = TreeWalk(repo) + headWalk.setRecursive(true) + headWalk.addTree(head.getTree()) val files: MutableMap> = mutableMapOf() // Build a map of file names and their code lines. - while (treeWalk.next()) { - val path = treeWalk.getPathString() - val fileLoader = repo.open(treeWalk.getObjectId(0)) + while (headWalk.next()) { + val path = headWalk.getPathString() + val fileId = headWalk.getObjectId(0) + val fileLoader = repo.open(fileId) if (!RawText.isBinary(fileLoader.openStream())) { val fileText = RawText(fileLoader.getBytes()) var lines = ArrayList(fileText.size()) for (idx in 0 .. fileText.size() - 1) { - lines.add(RevCommitLine(head, path, idx)) + lines.add(RevCommitLine(head, fileId, path, idx, false)) } files.put(path, lines) } } - - getDiffsObservable().blockingSubscribe { (commit, diffs) -> + + getDiffsObservable(tail).blockingSubscribe { (commit, diffs) -> // A step back in commits history. Update the files map according // to the diff. for (diff in diffs) { @@ -210,12 +237,12 @@ class CodeLongevity(private val localRepo: LocalRepo, continue } - // File was deleted, put its lines into the files map. + // File was deleted, initialize the line array in the files map. if (diff.changeType == DiffEntry.ChangeType.DELETE) { val fileLoader = repo.open(oldId) val fileText = RawText(fileLoader.getBytes()) - val lines = ArrayList(fileText.size()) - files.put(oldPath, lines) + files.put(oldPath, + ArrayList(fileText.size())) } // If a file was deleted, then the new path is /dev/null. @@ -238,15 +265,13 @@ class CodeLongevity(private val localRepo: LocalRepo, var insEnd = edit.getEndB() Logger.debug("ins ($insStart, $insEnd)") - val fileLoader = repo.open(newId) - val fileText = RawText(fileLoader.getBytes()) - for (idx in insStart .. insEnd - 1) { - val from = RevCommitLine(commit, newPath, idx) + val from = RevCommitLine(commit, newId, + newPath, idx, false) var to = lines.get(idx) - val cl = CodeLine(from, to, fileText.getString(idx)) - subscriber.onNext(cl) + val cl = CodeLine(repo, from, to) Logger.debug("Collected: ${cl.toString()}") + subscriber.onNext(cl) } lines.subList(insStart, insEnd).clear() } @@ -264,7 +289,8 @@ class CodeLongevity(private val localRepo: LocalRepo, var tmpLines = ArrayList(delCount) for (idx in delStart .. delEnd - 1) { - tmpLines.add(RevCommitLine(commit, oldPath, idx)) + tmpLines.add(RevCommitLine(commit, oldId, + oldPath, idx, true)) } lines.addAll(delStart, tmpLines) } @@ -282,12 +308,22 @@ class CodeLongevity(private val localRepo: LocalRepo, // them all into the result lines list, so the caller can update their // ages properly. if (tail != null) { - for ((file, lines) in files) { - for (idx in 0 .. lines.size - 1) { - val from = RevCommitLine(tail, file, idx) - val cl = CodeLine(from, lines[idx], - "no data (too lazy to compute)") - subscriber.onNext(cl) + val tailWalk = TreeWalk(repo) + tailWalk.setRecursive(true) + tailWalk.addTree(tail.getTree()) + + while (tailWalk.next()) { + val filePath = tailWalk.getPathString() + val lines = files.get(filePath) + if (lines != null) { + val fileId = tailWalk.getObjectId(0) + for (idx in 0 .. lines.size - 1) { + val from = RevCommitLine(tail, fileId, + filePath, idx, false) + val cl = CodeLine(repo, from, lines[idx]) + Logger.debug("Collected (tail): ${cl.toString()}") + subscriber.onNext(cl) + } } } } @@ -298,7 +334,8 @@ class CodeLongevity(private val localRepo: LocalRepo, /** * Iterates over the diffs between commits in the repo's history. */ - private fun getDiffsObservable(): Observable>> = + private fun getDiffsObservable(tail : RevCommit?) : + Observable>> = Observable.create { subscriber -> val revWalk = RevWalk(repo) diff --git a/src/test/kotlin/test/tests/hashers/CodeLongevityTest.kt b/src/test/kotlin/test/tests/hashers/CodeLongevityTest.kt index c23195a9..dee6e32d 100644 --- a/src/test/kotlin/test/tests/hashers/CodeLongevityTest.kt +++ b/src/test/kotlin/test/tests/hashers/CodeLongevityTest.kt @@ -41,7 +41,7 @@ class CodeLongevityTest : Spek({ /** * Assert function to test CodeLine object. */ - fun assertCodeLine(lineText: String, + fun assertCodeLine(lineText: String, isDeleted: Boolean, fromCommit: RevCommit, fromFile: String, fromLineNum: Int, toCommit: RevCommit, toFile: String, toLineNum: Int, actualLine: CodeLine) { @@ -50,11 +50,13 @@ class CodeLongevityTest : Spek({ "'$lineText' from_commit"); assertRevCommitLine(toCommit, toFile, toLineNum, actualLine.to, "'$lineText' to_commit"); + assertEquals(lineText, actualLine.text, "line text") + assertEquals(isDeleted, actualLine.isDeleted, "'$lineText' line is deleted") } - given("'test group #1'") { - val testRepoPath = "../testrepo1" + given("'line collecting #1'") { + val testRepoPath = "../CodeLongevity_lc1" val testRepo = TestRepo(testRepoPath) val fileName = "test1.txt" @@ -66,11 +68,11 @@ class CodeLongevityTest : Spek({ it("'t1: initial insertion'") { assertEquals(2, lines1.size) - assertCodeLine("line1", + assertCodeLine("line1", false, rev1, fileName, 0, rev1, fileName, 0, lines1[0]) - assertCodeLine("line2", + assertCodeLine("line2", false, rev1, fileName, 1, rev1, fileName, 1, lines1[1]) @@ -84,15 +86,15 @@ class CodeLongevityTest : Spek({ it("'t2: subsequent insertion'") { assertEquals(3, lines2.size) - assertCodeLine("line in the middle", + assertCodeLine("line in the middle", false, rev2, fileName, 1, rev2, fileName, 1, lines2[0]) - assertCodeLine("line1", + assertCodeLine("line1", false, rev1, fileName, 0, rev2, fileName, 0, lines2[1]) - assertCodeLine("line2", + assertCodeLine("line2", false, rev1, fileName, 1, rev2, fileName, 2, lines2[2]) @@ -106,15 +108,15 @@ class CodeLongevityTest : Spek({ it("'t3: subsequent deletion'") { assertEquals(3, lines3.size) - assertCodeLine("line in the middle", + assertCodeLine("line in the middle", false, rev2, fileName, 1, rev3, fileName, 1, lines3[0]) - assertCodeLine("line1", + assertCodeLine("line1", false, rev1, fileName, 0, rev3, fileName, 0, lines3[1]) - assertCodeLine("line2", + assertCodeLine("line2", true, rev1, fileName, 1, rev3, fileName, 2, lines3[2]) @@ -128,16 +130,16 @@ class CodeLongevityTest : Spek({ it("'t4: file deletion'") { assertEquals(3, lines4.size) - assertCodeLine("line in the middle", + assertCodeLine("line in the middle", true, rev2, fileName, 1, rev4, fileName, 1, lines4[0]) - assertCodeLine("line1", + assertCodeLine("line1", true, rev1, fileName, 0, rev4, fileName, 0, lines4[1]) - assertCodeLine("line2", + assertCodeLine("line2", true, rev1, fileName, 1, rev3, fileName, 2, lines4[2]) @@ -148,19 +150,18 @@ class CodeLongevityTest : Spek({ } } - given("'test group #2'") { - - val testRepoPath = "../testrepo2" + given("'line collecting #2'") { + val testRepoPath = "../CodeLongevity_lc2" val testRepo = TestRepo(testRepoPath) val fileName = "test1.txt" // t2.1: initial insertion val fileContent = listOf( + "line0", "line1", "line2", "line3", "line4", - "line4", "line5", "line6", "line7", @@ -184,7 +185,7 @@ class CodeLongevityTest : Spek({ it("'t2.1: initial insertion'") { assertEquals(fileContent.size, lines1.size) for (idx in 0 .. fileContent.size - 1) { - assertCodeLine(fileContent[idx], + assertCodeLine(fileContent[idx], false, rev1, fileName, idx, rev1, fileName, idx, lines1[idx]) @@ -192,6 +193,31 @@ class CodeLongevityTest : Spek({ } // t2.2: ins+del + + // Diff: + // 0 0 line0 + // 1 1 line1 + // 2 2 line2 + // 3 - line3 + // 4 - line4 + // 5 - line5 + // 3 + Proof addition 1 + // 6 4 line6 + // 7 5 line7 + // 8 6 line8 + // 9 - line9 + // 10 - line10 + // 11 - line11 + // 7 + Proof addition 2 + // 12 8 line12 + // 13 9 line13 + // 14 10 line14 + // 15 - line15 + // 16 - line16 + // 17 - line17 + // 18 - line18 + // 11 + Proof addition 3 + testRepo.deleteLines(fileName, 15, 18) testRepo.deleteLines(fileName, 9, 11) testRepo.deleteLines(fileName, 3, 5) @@ -205,24 +231,119 @@ class CodeLongevityTest : Spek({ it("'t2.2: ins+del'") { assertEquals(22, lines2.size) - assertCodeLine("Proof addition 3", rev2, fileName, 11, - rev2, fileName, 11, lines2[0]) - assertCodeLine("Proof addition 2", rev2, fileName, 7, + assertCodeLine("Proof addition 3", false, + rev2, fileName, 11, rev2, + fileName, 11, lines2[0]) + assertCodeLine("Proof addition 2", false, + rev2, fileName, 7, rev2, fileName, 7, lines2[1]) - assertCodeLine("Proof addition 1", rev2, fileName, 3, + assertCodeLine("Proof addition 1", false, + rev2, fileName, 3, rev2, fileName, 3, lines2[2]) - assertCodeLine("line1", rev1, fileName, 0, + assertCodeLine("line0", false, + rev1, fileName, 0, rev2, fileName, 0, lines2[3]) - assertCodeLine("line2", - rev1, fileName, 1, rev2, fileName, 1, lines2[4]) - assertCodeLine("line3", - rev1, fileName, 2, rev2, fileName, 2, lines2[5]) - assertCodeLine("line4", - rev1, fileName, 3, rev2, fileName, 3, lines2[6]) - assertCodeLine("line4", - rev1, fileName, 4, rev2, fileName, 4, lines2[7]) - assertCodeLine("line5", - rev1, fileName, 5, rev2, fileName, 5, lines2[8]) + assertCodeLine("line1", false, + rev1, fileName, 1, + rev2, fileName, 1, lines2[4]) + assertCodeLine("line2", false, + rev1, fileName, 2, + rev2, fileName, 2, lines2[5]) + assertCodeLine("line3", true, + rev1, fileName, 3, + rev2, fileName, 3, lines2[6]) + assertCodeLine("line4", true, + rev1, fileName, 4, + rev2, fileName, 4, lines2[7]) + assertCodeLine("line5", true, + rev1, fileName, 5, + rev2, fileName, 5, lines2[8]) + assertCodeLine("line6", false, + rev1, fileName, 6, + rev2, fileName, 4, lines2[9]) + assertCodeLine("line7", false, + rev1, fileName, 7, + rev2, fileName, 5, lines2[10]) + assertCodeLine("line8", false, + rev1, fileName, 8, + rev2, fileName, 6, lines2[11]) + assertCodeLine("line9", true, + rev1, fileName, 9, + rev2, fileName, 9, lines2[12]) + assertCodeLine("line10", true, + rev1, fileName, 10, + rev2, fileName, 10, lines2[13]) + assertCodeLine("line11", true, + rev1, fileName, 11, + rev2, fileName, 11, lines2[14]) + assertCodeLine("line12", false, + rev1, fileName, 12, + rev2, fileName, 8, lines2[15]) + } + + afterGroup { + testRepo.destroy() + } + } + + given("'line collecting #3: between revisions'") { + val testRepoPath = "../CodeLongevity_lc3" + val testRepo = TestRepo(testRepoPath) + val fileName = "test1.txt" + + testRepo.createFile(fileName, listOf("line1", "line2")) + val rev1 = testRepo.commit("inital commit") + testRepo.insertLines(fileName, 1, listOf("line15")) + val rev2 = testRepo.commit("insert line") + testRepo.deleteLines(fileName, 2, 2) + val rev3 = testRepo.commit("delete line2") + + val lines1 = CodeLongevity(LocalRepo(testRepoPath), Repo(), + MockApi(), testRepo.git).getLinesList() + val lines1_line15 = lines1[0] + val lines1_line1 = lines1[1] + val lines1_line2 = lines1[2] + + it("'before'") { + assertEquals(3, lines1.size) + assertCodeLine("line15", false, + rev2, fileName, 1, + rev3, fileName, 1, + lines1_line15) + assertCodeLine("line1", false, + rev1, fileName, 0, + rev3, fileName, 0, + lines1_line1) + assertCodeLine("line2", true, + rev1, fileName, 1, + rev3, fileName, 2, + lines1_line2) + } + + testRepo.deleteLines(fileName, 0, 0) + val rev4 = testRepo.commit("delete line1") + + val lines2 = CodeLongevity(LocalRepo(testRepoPath), Repo(), + MockApi(), testRepo.git).getLinesList(rev3) + val lines2_line1 = lines2[0] + val lines2_line15 = lines2[1] + + it("'after'") { + assertEquals(2, lines2.size) + assertCodeLine("line1", true, + rev3, fileName, 0, + rev4, fileName, 0, + lines2_line1) + assertEquals(lines1_line1.newId, lines2_line1.oldId, + "line1 old and new ids matching") + + assertCodeLine("line15", false, + rev3, fileName, 1, + rev4, fileName, 0, + lines2_line15) + assertEquals(lines1_line15.newId, lines2_line15.oldId, + "line15 old and new ids matching") + } afterGroup {