Skip to content

Commit 53e4094

Browse files
committed
fix: Prevent commits from long lived branches to be counted more than once (#60)
1 parent b06b01f commit 53e4094

File tree

8 files changed

+161
-73
lines changed

8 files changed

+161
-73
lines changed

src/main/kotlin/git/semver/plugin/scm/Commit.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package git.semver.plugin.scm
22

33
import java.util.Date
44

5-
class Commit(override val text: String, override val sha: String, val parents: Sequence<Commit>,
5+
class Commit(override val text: String, override val sha: String, val commitTime: Int, val parents: Sequence<Commit>,
66
val authorName:String = "", val authorEmail:String = "", val authorWhen:Date = Date()) : IRefInfo {
77
override fun toString(): String = text
88
}

src/main/kotlin/git/semver/plugin/scm/GitProvider.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,14 @@ internal class GitProvider(private val settings: SemverSettings) {
119119

120120
internal fun getHeadCommit(it: Repository): Commit {
121121
val revWalk = RevWalk(it)
122-
val head = it.resolve("HEAD") ?: return Commit("", "", emptySequence())
122+
val head = it.resolve("HEAD") ?: return Commit("", "", 0, emptySequence())
123123
val revCommit = revWalk.parseCommit(head)
124124
revWalk.markStart(revCommit)
125125
return getCommit(revCommit, revWalk)
126126
}
127127

128128
private fun getCommit(commit: RevCommit, revWalk: RevWalk): Commit {
129-
return Commit(commit.fullMessage, commit.name, sequence {
129+
return Commit(commit.fullMessage, commit.name, commit.commitTime, sequence {
130130
for (parent in commit.parents) {
131131
revWalk.parseHeaders(parent)
132132
yield(getCommit(parent, revWalk))

src/main/kotlin/git/semver/plugin/semver/VersionFinder.kt

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import git.semver.plugin.scm.Commit
44
import git.semver.plugin.scm.IRefInfo
55
import org.slf4j.LoggerFactory
66
import java.util.ArrayDeque
7+
import java.util.PriorityQueue
78

89
class VersionFinder(private val settings: SemverSettings, private val tags: Map<String, List<IRefInfo>>) {
910
private val logger = LoggerFactory.getLogger(javaClass)
@@ -51,62 +52,113 @@ class VersionFinder(private val settings: SemverSettings, private val tags: Map<
5152
return findVersion(sequenceOf(startCommit), changeLog)
5253
}
5354

55+
data class CommitData(
56+
val commit: Commit,
57+
val parents: MutableList<String>,
58+
val isParentOfReleaseCommit: Boolean
59+
) : Comparable<CommitData> {
60+
61+
override fun compareTo(other: CommitData): Int {
62+
return other.commit.commitTime.compareTo(commit.commitTime)
63+
}
64+
}
65+
5466
private fun findVersion(
5567
commitsList: Sequence<Commit>,
5668
changeLog: MutableList<Commit>?
5769
): MutableSemVersion {
5870

71+
var liveBranchCount = 1;
5972
var lastFoundVersion = versionZero()
73+
6074
// This code is a recursive algoritm rewritten as iterative to avoid stack overflow exception.
6175
// Unfortunately that makes it hard to understand.
62-
val commits = ArrayDeque(commitsList.map { it to ArrayList<String>(1) }.toList())
76+
77+
val commits = PriorityQueue(commitsList.map { CommitData(it, ArrayList(1), false) }.toList())
6378
val visitedCommits = mutableMapOf<String, MutableSemVersion?>()
79+
val includedCommits = ArrayDeque<CommitData>()
80+
6481
while (commits.isNotEmpty()) {
65-
val peek = commits.peek()
66-
val currentCommit = peek.first
67-
val currentParentList = peek.second
68-
if (!visitedCommits.containsKey(currentCommit.sha)) {
69-
// First time we visit this commit
82+
val commitData = commits.remove()
83+
val currentCommit = commitData.commit
84+
85+
// First time we visit this commit
86+
87+
if (commitData.isParentOfReleaseCommit) {
88+
// This commit is a parent of a release commit
89+
markParentCommitsAsVisited(liveBranchCount, currentCommit, visitedCommits, commits)
90+
} else if (!visitedCommits.containsKey(currentCommit.sha)) {
91+
7092
val releaseVersion = getReleaseSemVersionFromCommit(currentCommit)
7193
visitedCommits[currentCommit.sha] = releaseVersion
94+
7295
if (isRelease(releaseVersion)) {
7396
logger.debug("Release version found: {}", releaseVersion)
7497
// Release fond so no need to visit this commit again
75-
commits.pop()
7698
lastFoundVersion = releaseVersion!!
99+
100+
liveBranchCount -= 1
101+
102+
markParentCommitsAsVisited(liveBranchCount, currentCommit, visitedCommits, commits)
77103
} else {
104+
// This is a normal commit or a pre-release. We will visit this again in the second phase.
105+
includedCommits.push(commitData)
106+
78107
currentCommit.parents.forEach {
79-
currentParentList.add(it.sha)
108+
commitData.parents.add(it.sha)
80109
if (!visitedCommits.containsKey(it.sha)) {
81110
// prepare to visit parent commit
82-
commits.push(it to ArrayList(1))
111+
commits.add(CommitData(it, ArrayList(1), false))
83112
}
84113
}
114+
liveBranchCount += commitData.parents.size - 1
85115
}
86-
} else {
87-
// Second time we visit this commit after visiting parent commits
88-
addToChangeLog(currentCommit, changeLog, currentParentList.size > 1)
89-
90-
// Check if we found a preRelease version first time we visited this commit
91-
val preReleaseVersion = visitedCommits[currentCommit.sha]
92-
93-
// Get and clear the semVersions for the parents so that they are not counted twice
94-
val parentSemVersions = currentParentList
95-
.mapNotNull { visitedCommits.put(it, null) }
96-
.toList()
97-
98-
val maxVersionFromParents = parentSemVersions.maxOrNull() ?: versionZero()
99-
maxVersionFromParents.mergeChanges(parentSemVersions)
100-
maxVersionFromParents.updateFromCommit(currentCommit, settings, preReleaseVersion)
101-
visitedCommits[currentCommit.sha] = maxVersionFromParents
102-
103-
commits.pop()
104-
lastFoundVersion = maxVersionFromParents
105116
}
106117
}
118+
119+
while (includedCommits.isNotEmpty()) {
120+
121+
val commitData = includedCommits.pop()
122+
val currentCommit = commitData.commit
123+
val currentParentList = commitData.parents
124+
125+
// Second time we visit this commit after visiting parent commits
126+
addToChangeLog(currentCommit, changeLog, currentParentList.size > 1)
127+
128+
// Check if we found a preRelease version first time we visited this commit
129+
val preReleaseVersion = visitedCommits[currentCommit.sha]
130+
131+
// Get and clear the semVersions for the parents so that they are not counted twice
132+
val parentSemVersions = currentParentList
133+
.mapNotNull { visitedCommits.put(it, null) }
134+
.toList()
135+
136+
val maxVersionFromParents = parentSemVersions.maxOrNull() ?: versionZero()
137+
maxVersionFromParents.mergeChanges(parentSemVersions)
138+
maxVersionFromParents.updateFromCommit(currentCommit, settings, preReleaseVersion)
139+
visitedCommits[currentCommit.sha] = maxVersionFromParents
140+
141+
lastFoundVersion = maxVersionFromParents
142+
143+
}
107144
return lastFoundVersion
108145
}
109146

147+
private fun markParentCommitsAsVisited(
148+
liveBranchCount: Int,
149+
currentCommit: Commit,
150+
visitedCommits: MutableMap<String, MutableSemVersion?>,
151+
commits: PriorityQueue<CommitData>
152+
) {
153+
if (liveBranchCount == 0) {
154+
return
155+
}
156+
currentCommit.parents.filter { !visitedCommits.containsKey(it.sha) }.forEach {
157+
visitedCommits[it.sha] = null
158+
commits.add(CommitData(it, ArrayList(1), true))
159+
}
160+
}
161+
110162
private fun addToChangeLog(
111163
currentCommit: Commit,
112164
changeLog: MutableList<Commit>?,

src/test/kotlin/git/semver/plugin/changelog/ChangeLogFormatTest.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,11 @@ class ChangeLogFormatTest {
133133
fun format_no_grouping_nor_sorting() {
134134
val settings = SemverSettings()
135135
val changeLog = listOf(
136-
Commit("fix: B", "1", emptySequence()),
137-
Commit("fix: A", "2", emptySequence()),
138-
Commit("ignore: B", "5", emptySequence()),
139-
Commit("fix: B", "3", emptySequence()),
140-
Commit("fix: A", "4", emptySequence()),
136+
Commit("fix: B", "1", 0, emptySequence()),
137+
Commit("fix: A", "2", 1, emptySequence()),
138+
Commit("ignore: B", "5", 2, emptySequence()),
139+
Commit("fix: B", "3", 3, emptySequence()),
140+
Commit("fix: A", "4", 4, emptySequence()),
141141
)
142142
val c = ChangeLogTexts(mutableMapOf(
143143
"fix" to "FIX",
@@ -177,6 +177,6 @@ class ChangeLogFormatTest {
177177
"0100000" to "xyz: Some other change",
178178
"0110000" to "An uncategorized change"
179179
)
180-
return changeLog.map { Commit(it.value, it.key, emptySequence()) }
180+
return changeLog.map { Commit(it.value, it.key, 0, emptySequence()) }
181181
}
182182
}

src/test/kotlin/git/semver/plugin/gradle/GitSemverPluginExtensionTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class GitSemverPluginExtensionTest {
6262
}
6363

6464
val actual = semver.changeLogFormat.formatLog(listOf(
65-
Commit("test: Test Commit", "sha", emptySequence(), "John Doe", "john.doe@example.com", Date())),
65+
Commit("test: Test Commit", "sha", 0, emptySequence(), "John Doe", "john.doe@example.com", Date())),
6666
semver.createSettings(),
6767
semver.changeLogTexts)
6868

src/test/kotlin/git/semver/plugin/gradle/GitSemverPluginTest.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,19 @@ class GitSemverPluginTest {
113113
task.createRelease()
114114
}.doesNotThrowAnyException()
115115
}
116+
117+
@Test
118+
fun `plugin version for a git directory`() {
119+
val project = ProjectBuilder.builder().build()
120+
project.plugins.apply("com.github.jmongard.git-semver-plugin")
121+
val c = project.extensions.findByName("semver") as GitSemverPluginExtension
122+
123+
// c.gitDirectory.set(File("c:/dev/src/test1"))
124+
c.gitDirectory.set(project.layout.projectDirectory)
125+
126+
val task = project.tasks.findByName("printVersion") as PrintTask
127+
128+
assertThat(task).isNotNull()
129+
assertThatCode { task.print() }.doesNotThrowAnyException()
130+
}
116131
}

src/test/kotlin/git/semver/plugin/semver/MutableSemVersionTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class MutableSemVersionTest {
9090
fun revisionString() {
9191
val settings = SemverSettings()
9292
val semver = MutableSemVersion.tryParse(Tag("1.2.3", SHA))!!
93-
val commit = Commit("fix: a fix", SHA, emptySequence())
93+
val commit = Commit("fix: a fix", SHA, 0, emptySequence())
9494
semver.updateFromCommit(commit, settings, null)
9595
semver.updateFromCommit(commit, settings, null)
9696
semver.updateFromCommit(commit, settings, null)
@@ -99,8 +99,8 @@ class MutableSemVersionTest {
9999

100100
val actual = semver.toSemVersion().revisionString()
101101

102-
assertThat(actual).isEqualTo("1.2.3.4");
103-
assertThat(semver.toSemVersion()).hasToString("1.2.4+004.sha.8727a3e");
102+
assertThat(actual).isEqualTo("1.2.3.4")
103+
assertThat(semver.toSemVersion()).hasToString("1.2.4+004.sha.8727a3e")
104104
}
105105

106106
@ParameterizedTest

src/test/kotlin/git/semver/plugin/semver/SemVersionFinderTest.kt

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,20 @@ class SemVersionFinderTest {
7373
Tag("v1.3.1-RC", "SHA_B1")
7474
)
7575

76-
val a0 = Commit("fix: msg a0", "SHA_A0", sequenceOf())
77-
val a1 = Commit("fix: msg a1", "SHA_A1", sequenceOf(a0))
76+
val a0 = Commit("fix: msg a0", "SHA_A0", 0, sequenceOf())
77+
val a1 = Commit("fix: msg a1", "SHA_A1", 1, sequenceOf(a0))
7878

79-
val b0 = Commit("fix: msg b0", "SHA_B0", sequenceOf(a1))
80-
val b1 = Commit("fix: msg b1", "SHA_B1", sequenceOf(b0))
81-
val b2 = Commit("fix: msg b2", "SHA_B2", sequenceOf(b1))
79+
val b0 = Commit("fix: msg b0", "SHA_B0", 2, sequenceOf(a1))
80+
val b1 = Commit("fix: msg b1", "SHA_B1", 4, sequenceOf(b0))
81+
val b2 = Commit("fix: msg b2", "SHA_B2", 6, sequenceOf(b1))
8282

83-
val c0 = Commit("fix: msg c0", "SHA_C0", sequenceOf(a1))
84-
val c1 = Commit("fix: msg c1", "SHA_C1", sequenceOf(c0))
85-
val c2 = Commit("fix: msg c2", "SHA_C2", sequenceOf(c1))
86-
val c3 = Commit("fix: msg c3", "SHA_C3", sequenceOf(c2))
83+
val c0 = Commit("fix: msg c0", "SHA_C0", 3, sequenceOf(a1))
84+
val c1 = Commit("fix: msg c1", "SHA_C1", 5, sequenceOf(c0))
85+
val c2 = Commit("fix: msg c2", "SHA_C2", 7, sequenceOf(c1))
86+
val c3 = Commit("fix: msg c3", "SHA_C3", 9, sequenceOf(c2))
8787

88-
val d0 = Commit("fix: msg d0", "SHA_D0", sequenceOf(c3, b2))
89-
val d1 = Commit("fix: msg d1", "SHA_D1", sequenceOf(d0))
88+
val d0 = Commit("fix: msg d0", "SHA_D0", 10, sequenceOf(c3, b2))
89+
val d1 = Commit("fix: msg d1", "SHA_D1", 11, sequenceOf(d0))
9090

9191
// when
9292
val versions = getVersion(tags, d1)
@@ -103,19 +103,19 @@ class SemVersionFinderTest {
103103
Tag("v0.4.0", "SHA0")
104104
)
105105

106-
val a0 = Commit("a msg1", "SHA0", sequenceOf())
107-
val a1 = Commit("feat: a feature", "SHA1", sequenceOf(a0))
108-
val a2 = Commit("a msg3", "SHA2", sequenceOf(a1))
106+
val a0 = Commit("a msg1", "SHA0", 0, sequenceOf())
107+
val a1 = Commit("feat: a feature", "SHA1", 1, sequenceOf(a0))
108+
val a2 = Commit("a msg3", "SHA2", 2, sequenceOf(a1))
109109

110-
val b0 = Commit("fix: test 11", "SHA11", sequenceOf(a2))
111-
val b1 = Commit("fix: test 12", "SHA12", sequenceOf(b0))
112-
val b2 = Commit("fix: test 13", "SHA13", sequenceOf(b1))
110+
val b0 = Commit("fix: test 11", "SHA11", 3, sequenceOf(a2))
111+
val b1 = Commit("fix: test 12", "SHA12", 4, sequenceOf(b0))
112+
val b2 = Commit("fix: test 13", "SHA13", 5, sequenceOf(b1))
113113

114-
val c0 = Commit("fix: test 21", "SHA21", sequenceOf(a2))
115-
val c1 = Commit("fix: test 22", "SHA22", sequenceOf(c0))
114+
val c0 = Commit("fix: test 21", "SHA21", 6, sequenceOf(a2))
115+
val c1 = Commit("fix: test 22", "SHA22", 7, sequenceOf(c0))
116116

117-
val d0 = Commit("merge msg", "SHA31", sequenceOf(b2, c1))
118-
val d1 = Commit("fix: msg", "SHA32", sequenceOf(d0))
117+
val d0 = Commit("merge msg", "SHA31", 8, sequenceOf(b2, c1))
118+
val d1 = Commit("fix: msg", "SHA32", 9, sequenceOf(d0))
119119

120120
// when
121121
val versions = getVersion(tags, d1, groupVersions = false)
@@ -134,14 +134,14 @@ class SemVersionFinderTest {
134134
Tag("v0.4.2-Alpha.1", "SHA12"),
135135
)
136136

137-
val a0 = Commit("a msg1", "SHA0", sequenceOf())
138-
val a1 = Commit("a msg2", "SHA1", sequenceOf(a0))
139-
val a2 = Commit("a msg3", "SHA2", sequenceOf(a1))
137+
val a0 = Commit("a msg1", "SHA0", 0, sequenceOf())
138+
val a1 = Commit("a msg2", "SHA1", 2, sequenceOf(a0))
139+
val a2 = Commit("a msg3", "SHA2", 3, sequenceOf(a1))
140140

141-
val b0 = Commit("fix: test 11", "SHA11", sequenceOf(a2))
142-
val b1 = Commit("fix: test 12", "SHA12", sequenceOf(b0))
143-
val b2 = Commit("fix: test 13", "SHA13", sequenceOf(b1))
144-
val b3 = Commit("fix: msg", "SHA14", sequenceOf(b2))
141+
val b0 = Commit("fix: test 11", "SHA11", 4, sequenceOf(a2))
142+
val b1 = Commit("fix: test 12", "SHA12", 5, sequenceOf(b0))
143+
val b2 = Commit("fix: test 13", "SHA13", 6, sequenceOf(b1))
144+
val b3 = Commit("fix: msg", "SHA14", 7, sequenceOf(b2))
145145

146146
// when
147147
val versions = getVersion(tags, b3, groupVersions = false)
@@ -433,6 +433,27 @@ class SemVersionFinderTest {
433433
assertEquals("1.0.0-RC.3+001", actual.toInfoVersionString())
434434
}
435435

436+
@Test
437+
fun `test long lived develop branch should not count commits twice`() {
438+
val m1 = Commit("Initial commit", "1", 1, emptySequence())
439+
val d2 = Commit("fix: a fix", "2", 2, sequenceOf(m1))
440+
val d3 = Commit("feat: a feat", "3", 3, sequenceOf(d2))
441+
val m4 = Commit("merge branch develop", "4", 4, sequenceOf(m1, d3))
442+
val m5 = Commit("release: v1.0.0", "5", 5, sequenceOf(m4))
443+
val d6 = Commit("merge branch master into develop", "6", 6, sequenceOf(d3, m5))
444+
val d7 = Commit("feat: a feat", "7", 7, sequenceOf(d6))
445+
val d8 = Commit("feat: a feat", "8", 8, sequenceOf(d7))
446+
val m9 = Commit("merge branch develop", "9", 9, sequenceOf(m5,d8))
447+
val m10 = Commit("docs: some doc", "10", 10, sequenceOf(m9))
448+
val m11 = Commit("release: 1.1.0", "11", 11, sequenceOf(m10))
449+
val d12 = Commit("merge branch master into develop", "12", 12, sequenceOf(d8, m11))
450+
451+
val actual = getVersion(emptyList(), d12, false)
452+
453+
assertEquals("1.1.1-SNAPSHOT+001", actual.toInfoVersionString())
454+
}
455+
456+
436457
@Test
437458
fun testIncrementVersion_dirty() {
438459
assertEquals("1.1.1-SNAPSHOT", getVersionFromTagAndDirty("v1.1.0"))
@@ -525,12 +546,12 @@ class SemVersionFinderTest {
525546

526547
private fun asCommit(commits: List<String>) = asCommits(commits.reversed()).first()
527548

528-
private fun asCommits(commits: List<String>): Sequence<Commit> {
529-
return commits.take(1).map { Commit("commit message", it, asCommits(commits.drop(1))) }.asSequence()
549+
private fun asCommits(commits: List<String>, commitTime: Int = 0): Sequence<Commit> {
550+
return commits.take(1).map { Commit("commit message", it, commitTime, asCommits(commits.drop(1), commitTime + 1)) }.asSequence()
530551
}
531552

532-
private fun asCommits(shas: Iterable<Pair<String, String>>): Sequence<Commit> {
533-
return shas.take(1).map { Commit(it.second, it.first, asCommits(shas.drop(1))) }.asSequence()
553+
private fun asCommits(shas: Iterable<Pair<String, String>>, commitTime: Int = 0): Sequence<Commit> {
554+
return shas.take(1).map { Commit(it.second, it.first, commitTime, asCommits(shas.drop(1), commitTime + 1)) }.asSequence()
534555
}
535556

536557
private fun generateSHAString(range: IntRange): List<String> {

0 commit comments

Comments
 (0)