Skip to content

Commit 2bd0a7d

Browse files
Merge pull request #45 from AshleighAdams/follow-all-branches
HeightCalculator: Ensure all parents are inspected for height calculation
2 parents 155a850 + 753ff0f commit 2bd0a7d

11 files changed

+429
-108
lines changed

.github/shared.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ definitions:
3838

3939
test: &test
4040
name: Test
41-
run: dotnet test --configuration Release --no-build --logger GitHubActions -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura
41+
run: dotnet test --configuration Debug --logger GitHubActions -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura
4242

4343
mutation-test: &mutation-test
4444
name: Mutation Test

.github/workflows/continuous-delivery.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
run: dotnet build --configuration Release --no-restore
4343

4444
- name: Test
45-
run: dotnet test --configuration Release --no-build --logger GitHubActions -p:CollectCoverage=true
45+
run: dotnet test --configuration Debug --logger GitHubActions -p:CollectCoverage=true
4646
-p:CoverletOutputFormat=cobertura
4747

4848
- name: Pack

.github/workflows/continuous-integration.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
run: dotnet build --configuration Release --no-restore
4343

4444
- name: Test
45-
run: dotnet test --configuration Release --no-build --logger GitHubActions -p:CollectCoverage=true
45+
run: dotnet test --configuration Debug --logger GitHubActions -p:CollectCoverage=true
4646
-p:CoverletOutputFormat=cobertura
4747

4848
- name: Pack

.github/workflows/deploy-release.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
run: dotnet build --configuration Release --no-restore
5050

5151
- name: Test
52-
run: dotnet test --configuration Release --no-build --logger GitHubActions -p:CollectCoverage=true
52+
run: dotnet test --configuration Debug --logger GitHubActions -p:CollectCoverage=true
5353
-p:CoverletOutputFormat=cobertura
5454

5555
- name: Pack

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Versioning based upon commit messages or branches is out of scope. Such can be d
3434

3535
## Version Calculation
3636

37-
Take the head commit, if one or more version tags exist, use the highest version, otherwise, keep following the first parent of each commit until a version tag is found, taking the highest version tag, then bumping the version and appending the "commit height" onto the end.
37+
Take the head commit, if one or more version tags exist, use the highest version, otherwise, keep following all parents until a version tag is found, taking the highest version tag, then bumping the version and appending the "commit height" onto the end.
3838

3939
To bump the version, the patch is by default incremented by 1. The version part to bump can be configured via `VerliteAutoIncrement`/`--auto-increment` option.
4040

@@ -64,7 +64,7 @@ See [docs/VersionCalculation.md](docs/VersionCalculation.md) for further reading
6464

6565
GitVersion has a focus on branches, and is well suited for a Continuous Deployment workflow, where releases are triggered based upon branches or commit messages. Shallow repositories are not supported.
6666

67-
Verlite cares only about tags—more specifically, the tags on the chain of first parents—and is well suited for Continuous Delivery workflows, where official releases happen by tagging.
67+
Verlite cares only about tags, and is well suited for Continuous Delivery workflows, where official releases happen by tagging.
6868

6969
## Comparison with MinVer
7070

docs/Assets/VersionGraph.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// generate from here: https://codepen.io/nicoespeon/pen/arqPWb
2+
13
const template = GitgraphJS.templateExtend(GitgraphJS.TemplateName.Metro, {
24
commit: {
35
message: {
@@ -28,7 +30,7 @@ const featureA = master.branch("some-fix")
2830
.commit("Versioned as: 1.0.1-alpha.2")
2931
.commit("Versioned as: 1.0.1-alpha.3");
3032

31-
featureC.merge(master, "Versioned as: 0.1.1-alpha.2");
33+
featureC.merge(master, "Versioned as: 1.0.1-alpha.1");
3234

3335
const featureB = master.branch("new-fix")
3436
.commit("Versioned as: 1.0.1-alpha.1");

docs/Assets/VersionGraph.svg

+62-62
Loading

docs/VersionCalculation.md

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
# Version Calculation In Depth
22

3-
To calculate a version, Verlite takes the head commit for the "current commit," and until the "current commit" is tagged with a version, will keep checking the *first* parent only. Once a commit with a version tag is found, the highest tag is used for calculating future versions. If the number of commits traversed is zero, the version specified in the tag is used verbatim, otherwise the version is bumped, and a height appended. If the last tag was a prerelease, then the height will be appended in full to that prerelease, otherwise the default phase will be used ("alpha").
3+
To calculate a version, Verlite takes the head commit for the "current commit," and until the "current commit" is tagged with a version, will keep checking parents. Once a commit with a version tag is found, the highest tag is used for calculating future versions. If the number of commits traversed is zero, the version specified in the tag is used verbatim, otherwise if the tag is a stable release, the version is bumped, appending a prerelease label (by default "alpha") and height, else the height with a separator will a be appended in full to to the previous tag's version (including prerelease label) with no major/minor/patch version bump.
44

55
The graph below should give you a good idea for how things are versioned.
66

77
![](Assets/VersionGraph.svg)
88

9-
Take note how the `old-feature` branch predates the version, taking only the version from when it originally branched, even after merging in a commit. It is for this reason squashing or merge commits should always be used, as only the first parent is taken into account upon version calculation.
9+
Note how there are multiple commits versioned as `1.0.1-alpha.n`, this is a result of both: commits in Git not being on a branch; and Verlite being branch-blind—this is expected behavior, and in no way an error. Builds with "height" should not be released beyond development channels such as nightly builds for internal testing. These "nightly" releases should be done so only from a specific branch (such as `master` and/or `support/*`) in order for deliverables to maintain a monotonic commit height.
1010

11-
Note how there are multiple commits versioned as `1.0.1-alpha.n`, this is a result of both: commits in Git not being on a branch; and Verlite being branch-blind—this is expected behavior, and in no way an error. Builds with "height" should not be released beyond the expectation of nightly/CI builds for testing, and should development releases only be done from a given branch (such as `master` and/or `support/*`) then the deliverables with height will retain a monotonic version.
12-
13-
The same commit can be released multiple times should it be tagged again with a higher version. In practice, this will most often be done with release candidates, and can be seen in the graph above, where the `HEAD` commit was tagged and released under both `1.0.1-rc.2` and `1.0.1`.
11+
The same commit can be released multiple times if tagged again with a higher version. In practice, this will most often be done with release candidates, and can be seen in the graph above, where the `HEAD` commit was tagged and released under both `1.0.1-rc.2` and `1.0.1`.

src/Verlite.Core/HeightCalculator.cs

+91-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

22
using System;
33
using System.Collections.Generic;
4+
using System.Diagnostics;
45
using System.Linq;
56
using System.Threading.Tasks;
67

@@ -57,48 +58,120 @@ private static IEnumerable<TaggedVersion> SelectWhereSemver(
5758
if (head is null)
5859
return (1, null);
5960

60-
var current = head.Value;
61-
int height = 0;
62-
while (true)
61+
return await FromCommit(
62+
commit: head.Value,
63+
commitDescriptor: "HEAD",
64+
options: new FromCommitOptions(repo, tags, tagPrefix, log, tagFilter));
65+
}
66+
67+
private class FromCommitOptions
68+
{
69+
public IRepoInspector Repo { get; }
70+
public TagContainer Tags { get; }
71+
public string TagPrefix { get; }
72+
public ILogger? Log { get; }
73+
public ITagFilter? TagFilter { get; }
74+
75+
public FromCommitOptions(
76+
IRepoInspector repo,
77+
TagContainer tags,
78+
string tagPrefix,
79+
ILogger? log,
80+
ITagFilter? tagFilter)
6381
{
64-
var currentTags = tags.FindCommitTags(current);
82+
Repo = repo;
83+
Tags = tags;
84+
TagPrefix = tagPrefix;
85+
Log = log;
86+
TagFilter = tagFilter;
87+
}
88+
89+
}
90+
91+
private static async Task<(int height, TaggedVersion? version)> FromCommit(
92+
Commit commit,
93+
string commitDescriptor,
94+
FromCommitOptions options)
95+
{
96+
var visited = new HashSet<Commit>();
97+
var toVisit = new Stack<(Commit commit, int height, string descriptor, int heightSinceBranch)>();
98+
toVisit.Push((commit, 0, commitDescriptor, 0));
99+
100+
var candidates = new List<(int height, TaggedVersion? version)>();
101+
102+
while (toVisit.Count > 0)
103+
{
104+
var (current, height, rootDescriptor, heightSinceBranch) = toVisit.Pop();
105+
106+
var descriptor = heightSinceBranch == 0 ? rootDescriptor : $"{rootDescriptor}~{heightSinceBranch}";
107+
108+
// already visited in an ultimately prior parent
109+
if (!visited.Add(current))
110+
{
111+
options.Log?.Verbatim($"{descriptor} found in prior parent, discontinuing branch.");
112+
continue;
113+
}
114+
115+
var currentTags = options.Tags.FindCommitTags(current);
65116
var versions = currentTags
66-
.Where(t => t.Name.StartsWith(tagPrefix, StringComparison.Ordinal))
67-
.SelectWhereSemver(tagPrefix, log)
117+
.Where(t => t.Name.StartsWith(options.TagPrefix, StringComparison.Ordinal))
118+
.SelectWhereSemver(options.TagPrefix, options.Log)
68119
.OrderByDescending(v => v.Version)
69120
.ToList();
70121

71-
log?.Verbatim($"HEAD^{height} {current} has {currentTags.Count} total tags with {versions.Count} versions.");
122+
options.Log?.Verbatim($"{descriptor} has {currentTags.Count} total tags with {versions.Count} versions.");
72123

73124
foreach (var tag in currentTags)
74-
log?.Verbatim($" found tag: {tag.Name}");
125+
options.Log?.Verbatim($" found tag: {tag.Name}");
75126

76127
List<TaggedVersion>? filteredVersions = null;
77128
foreach (var version in versions)
78129
{
79-
bool passesFilter = tagFilter is null || await tagFilter.PassesFilter(version);
130+
bool passesFilter = options.TagFilter is null || await options.TagFilter.PassesFilter(version);
80131

81132
if (passesFilter)
82133
{
83-
log?.Verbatim($" version candidate: {version.Version}");
134+
options.Log?.Verbatim($" version candidate: {version.Version}");
84135
filteredVersions ??= new();
85136
filteredVersions.Add(version);
86137
}
87138
else
88-
log?.Verbatim($" version filtered: {version.Version} (from tag {version.Tag.Name})");
139+
options.Log?.Verbatim($" version filtered: {version.Version} (from tag {version.Tag.Name})");
89140
}
90141

91142
if (filteredVersions is not null)
92-
return (height, filteredVersions.First());
143+
{
144+
var candidateVersion = filteredVersions[0];
145+
candidates.Add((height, candidateVersion));
146+
options.Log?.Verbose($"Candidate version {candidateVersion.Version} found with {height} height at {descriptor}.");
147+
continue;
148+
}
149+
150+
var parents = await options.Repo.GetParents(current);
93151

94-
height++;
95-
var parent = await repo.GetParent(current);
96-
if (parent is null)
97-
break;
98-
current = parent.Value;
152+
if (parents.Count == 0)
153+
{
154+
int phantomCommitHeight = height + 1;
155+
candidates.Add((phantomCommitHeight, null));
156+
}
157+
else
158+
{
159+
for (int i = parents.Count; i-- > 0;)
160+
{
161+
if (i == 0)
162+
toVisit.Push((parents[i], height + 1, rootDescriptor, heightSinceBranch + 1));
163+
else
164+
toVisit.Push((parents[i], height + 1, $"{rootDescriptor}^{i}", 0));
165+
}
166+
}
99167
}
100168

101-
return (height, null);
169+
Debug.Assert(candidates.Count > 0);
170+
171+
return candidates
172+
.OrderByDescending(x => x.version is not null)
173+
.ThenByDescending(x => x.version?.Version)
174+
.First();
102175
}
103176
}
104177
}

0 commit comments

Comments
 (0)