Skip to content

Commit

Permalink
major: Allow incrementing by higher scope than requested
Browse files Browse the repository at this point in the history
Our prior parallel version logic allowed reckon to increment by the
requested scope a second time in order to avoid a parallel
version. However, if that version is also in the parallel branch, it
would fail saying the version was already claimed.

In the new logic, if we bump the target normal in order to avoid a
parallel version and that version is still in the parallel versions
list, we increment increment by a higher scope (i.e. by MAJOR if they
requested MINOR or by MINOR if they requested PATCH).

This may resolve many of the bugs we had with parallel version
handling.

The two unintuitive parts are that it may still not increment as far
as someone wants in some cases. And in others someone could be
surprised that we incremented by a higher scope than they asked for.

To deal with the latter, we may want to consider making a distinction
between "soft" and "hard" scopes (i.e. did they explicitly ask for the
scope or did it get inferred). This was clearer in the past, when
"inferred" really only meant no input from the scope calc. However,
with the new commit message scope calc, that's really more of a "soft"
scope request than an explicit one. It's trickier because to the
Reckoner there's no difference between commit message scope calcs and
explicit user-requested scope calcs.

Will run this by the users and see what they think.

Fixes #180
Related to #102
  • Loading branch information
ajoberstar committed Aug 14, 2022
1 parent 9f53cbf commit ee8c65f
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,16 @@ private Version reckonNormal(VcsInventory inventory) {

// if a version's already being developed on a parallel branch we'll skip it
if (inventory.getParallelNormals().contains(targetNormal)) {
logger.debug("Skipping {} as it's being developed on a parallel branch. Incrementing again with {}", targetNormal, scope);
targetNormal = targetNormal.incrementNormal(scope);
}

// if it's still in parallel, increment with higher scope

if (inventory.getParallelNormals().contains(targetNormal) && scope != Scope.MAJOR) {
// TODO maybe only do this for "soft" scopes (i.e. not explicitly asked for)
logger.debug("Skipping {} as it's being developed on a parallel branch. Incrementing again with {}", targetNormal, scope);
targetNormal = targetNormal.incrementNormal(scope.increment());
}

return targetNormal;
Expand Down
13 changes: 13 additions & 0 deletions reckon-core/src/main/java/org/ajoberstar/reckon/core/Scope.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@
public enum Scope {
MAJOR, MINOR, PATCH;

public Scope increment() {
switch (this) {
case MAJOR:
throw new IllegalStateException("Cannot increment MAJOR");
case MINOR:
return MAJOR;
case PATCH:
return MINOR;
default:
throw new AssertionError("Invalid scope: " + this);
}
}

/**
* Parses a String version of a Scope. This is an alternative to {@code valueOf} which only supports
* literal matches. This method supports mixed case String representations, like Major or minor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,38 +45,6 @@ class ReckonerIntegTest extends Specification {
reckonStage(null, null) == '0.1.0'
}

@Ignore
def 'multiple release branches'() {
given:
commit()
branch('release/2.0.x')
checkout('release/2.0.x')
commit()
tag('2.0.0')
commit()
tag('2.0.1')
checkout('2.0.0')
branch('release/2.1.x')
checkout('release/2.1.x')
commit()
checkout('2.0.0')
branch('release/3.0.x')
checkout('release/3.0.x')
commit()
tag('3.0.0-dev.0')
commit()
tag('3.0.0-rc.1')
checkout('2.0.0')
branch('release/3.1.x')
checkout('release/3.1.x')
commit()
tag('3.1.0-dev.0')
commit()

expect:
reckonStage(null, null) == '3.1.0-dev.1.1+20180704T171826Z'
}

def setup() {
repoDir = Files.createTempDirectory('repo').toFile()
grgit = Grgit.init(dir: repoDir)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package org.ajoberstar.reckon.core

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;

import java.nio.file.Files
import java.security.SecureRandom
import org.ajoberstar.grgit.Grgit
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Ignore
import spock.lang.Stepwise

@Stepwise
class ReckonerMaintenanceBranchIntegTest extends Specification {
private static final Clock CLOCK = Clock.fixed(Instant.ofEpochSecond(1530724706), ZoneId.of('UTC'))
private static final String TIMESTAMP = '20180704T171826Z'

@Shared File repoDir
@Shared Grgit grgit
@Shared String initialBranch
@Shared SecureRandom random = new SecureRandom()

def 'reckon 1.0.0 version'() {
expect:
commit()
def version = reckonStage('major', 'final')
version == '1.0.0'
tag(version)
}

def 'reckon 1.1.0 version'() {
expect:
commit()
commit()
def dev1 = reckonStage('minor', 'beta')
dev1 == '1.1.0-beta.1'
tag(dev1)

commit()
commit()
def dev2 = reckonStage('minor', 'beta')
dev2 == '1.1.0-beta.2'
tag(dev2)

commit()
def dev3 = reckonStage('minor', 'beta')
dev3 == '1.1.0-beta.3'
tag(dev3)

def minor1 = reckonStage('minor', 'final')
minor1 == '1.1.0'
tag(minor1)
}

def 'reckon 1.1.1 version'() {
expect:
branch('maintenance/1.1.x')
checkout('maintenance/1.1.x')

commit()
def patch1 = reckonStage('patch', 'final')
patch1 == '1.1.1'
tag(patch1)
}

def 'reckon 1.2.0 version'() {
expect:
checkout(initialBranch)

commit()
commit()
commit()
def beta1 = reckonStage('minor', 'beta')
beta1 == '1.2.0-beta.1'
tag(beta1)

commit()
commit()
def beta2 = reckonStage('minor', 'beta')
beta2 == '1.2.0-beta.2'
tag(beta2)

def minor2 = reckonStage('minor', 'final')
minor2 == '1.2.0'
tag(minor2)
}

def 'reckon 1.2.x versions'() {
expect:
branch('maintenance/1.2.x')
checkout('maintenance/1.2.x')

commit()
def patch1 = reckonStage('patch', 'final')
patch1 == '1.2.1'
tag(patch1)

commit()
def patch2 = reckonStage('patch', 'final')
patch2 == '1.2.2'
tag(patch2)

commit()
def patch3 = reckonStage('patch', 'final')
patch3 == '1.2.3'
tag(patch3)
}

def 'reckon 1.3.x versions'() {
expect:
checkout(initialBranch)

commit()
commit()
commit()
def beta1 = reckonStage('patch', 'beta')
beta1 == '1.3.0-beta.1'
tag(beta1)
}

def setupSpec() {
repoDir = Files.createTempDirectory('repo').toFile()
grgit = Grgit.init(dir: repoDir)
initialBranch = grgit.branch.current().name
}

def cleanupSpec() {
grgit.close()
assert !repoDir.exists() || repoDir.deleteDir()
}

private String reckonStage(scope, stage) {
return Reckoner.builder()
.clock(CLOCK)
.git(grgit.repository.jgit.repository)
.scopeCalc { i -> Optional.ofNullable(scope) }
.stages('beta', 'milestone', 'rc', 'final')
.stageCalc { i, v -> Optional.ofNullable(stage) }
.build()
.reckon();
}

private String reckonSnapshot(scope, stage) {
return Reckoner.builder()
.clock(CLOCK)
.git(grgit.repository.jgit.repository)
.scopeCalc { i -> Optional.ofNullable(scope) }
.snapshots()
.stageCalc { i, v -> Optional.ofNullable(stage) }
.build()
.reckon();
}

private void commit() {
byte[] bytes = new byte[128]
random.nextBytes(bytes)
int fileName = random.nextInt();
new File(grgit.repository.rootDir, "${fileName}.txt") << bytes
grgit.add(patterns: ["${fileName}.txt"])
def commit = grgit.commit(message: 'do')
println "Created commit: ${commit.abbreviatedId}"
}

private void branch(String name) {
def currentHead = grgit.head()
def currentBranch = grgit.branch.current
def newBranch = grgit.branch.add(name: name)
def atCommit = grgit.resolve.toCommit(newBranch.fullName)
println "Added new branch ${name} at ${atCommit.abbreviatedId}"
assert currentBranch == grgit.branch.current
assert currentHead == atCommit
}

private void tag(String name, boolean annotate = true) {
def currentHead = grgit.head()
def newTag = grgit.tag.add(name: name, annotate: annotate)
def atCommit = grgit.resolve.toCommit(newTag.fullName)
println "Added new tag ${name} at ${atCommit.abbreviatedId}"
assert currentHead == atCommit
}

private void checkout(String name) {
def currentHead = grgit.head()
grgit.checkout(branch: name)
def atCommit = grgit.resolve.toCommit(name)
println "Checkout out ${name}, which is at ${atCommit.abbreviatedId}"
assert name == grgit.branch.current.name || grgit.branch.current.name == 'HEAD'
}

private void merge(String name) {
def currentBranch = grgit.branch.current.name
grgit.merge(head: name)
println "Merged ${name} into ${currentBranch}"
assert currentBranch == grgit.branch.current.name
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,22 @@ class ReckonerTest extends Specification {
reckonStage(inventory, 'major', 'final') == '3.0.0'
}

def 'if incremented via parallel still in parallel, increment with higher scoe '() {
given:
def inventory = new VcsInventory(
'abcdef',
true,
null,
Version.valueOf('1.1.0'),
Version.valueOf('1.1.0'),
1,
[Version.valueOf('1.1.1'), Version.valueOf('1.1.2')] as Set,
[Version.valueOf('1.1.0'), Version.valueOf('1.1.1'), Version.valueOf('1.1.2')] as Set
)
expect:
reckonStage(inventory, 'patch', 'final') == '1.2.0'
}

def 'if target normal is in the claimed versions, throw'() {
given:
def inventory = new VcsInventory(
Expand Down

0 comments on commit ee8c65f

Please sign in to comment.