GitVersion icon indicating copy to clipboard operation
GitVersion copied to clipboard

[Bug] `track-merge-changes` produces unexpected result when combining `hotfix` and `support` branches

Open Crown0815 opened this issue 3 years ago • 23 comments

Describe the bug

It appears that track-merge-changes does not work when combining hotfix and support branches. I have the following test

[Test]
public void MyTest()
{
    var config = new Config();
    config.Branches.Add("hotfix", new BranchConfig
    {
        TrackMergeTarget = true,
    });

    using var fixture = new EmptyRepositoryFixture();
    fixture.Repository.MakeACommit();
    fixture.Checkout(MainBranch);
    fixture.Repository.ApplyTag("1.7.0");
    
    fixture.BranchTo("hotfix/1.7");
    fixture.Repository.MakeCommits(1);
    fixture.Repository.ApplyTag("1.7.1-beta.1");

    fixture.Checkout(MainBranch);
    fixture.BranchTo("support/1.7");
    fixture.Repository.Merge(fixture.Repository.Branches["hotfix/1.7"], new Signature("a", "b", DateTimeOffset.Now-TimeSpan.FromMinutes(55)), new MergeOptions()
    {
        FastForwardStrategy = FastForwardStrategy.NoFastForward,
    });

    fixture.Repository.ApplyTag("1.7.1");

    fixture.Checkout("hotfix/1.7");
    fixture.Repository.MakeACommit("semver: bug");

    fixture.AssertFullSemver("1.7.2-beta.1", config);
}

that produces the following repository:

* d9e96de 54 minutes ago  (HEAD -> hotfix/1.7)
|   
| *  57c51f7 55 minutes ago  (tag: 1.7.1, support/1.7)
|/|   
* | 79a0af5 56 minutes ago  (tag: 1.7.1-beta.1)
|/  
* 2d1d766 58 minutes ago  (tag: 1.7.0, main)

Expected Behavior

hotfix is set to track-merge-targets = true; hence, I would expect its next version to be 1.7.2-beta.1

Actual Behavior

Running the test produces:

should be 1.7.2-beta.1 but was 1.7.1-beta.2+2

Possible Fix

I suspect the merge target tracking is incomplete, or I am not configuring GitVersion correctly.

Steps to Reproduce

See test above

Context

I am attempting to merge hotfix branches into support branches and have the version of the hotfix branches bumped without requiring a merge back from support into hotfix.

Your Environment

Standard git repository

  • Version Used: 5.3.6
  • Operating System and version (Windows 10):
  • Link to your project: N/A
  • Link to your CI build (if appropriate): N/A

Crown0815 avatar Mar 17 '22 07:03 Crown0815

I'm not sure, but I think the problem may be that the tag 1.7.1 is applied to the support/1.7 branch and not to main. I don't think support branches are considered merge targets for hotfix by default. If you apply the 1.7.1 tag to main, I think you should get the expected version 1.7.2-beta.1.

asbjornu avatar Mar 18 '22 13:03 asbjornu

@asbjornu yes, but main is not on version 1.7.1, it is still on version 1.7.0 in our case, so that is not an option. Can merge targets be configured?

Crown0815 avatar Apr 13 '22 08:04 Crown0815

You can configure source-branches as a sort of opposite relationship to "merge target".

asbjornu avatar Apr 13 '22 12:04 asbjornu

I'm not sure what your branching and merging strategy is exactly but it seems to be that the gitflow strategy maybe a good solution for you (see gitflow example here). Anyway if you ask me you should on the one hand create a dedicated hotfix branch from the source branch you want to place the bug fix and on the other hand create a hotfix branch with the target semantic version number (e.g. hotfix/1.7.1). At this moment you have tested your hotfix and ensure the stability of the release candidate (RC), you are going to merge the hotfix to the support branch (support/1.7) and delete the hotfix branch. That’s mean you declare this fix as resolved and released it to manufacturing (RTM) (e.g. tagging this commit with 1.7.1). Now if you have another bug (which needs to be hot fixed) you create an additional branch from the support branch with the name hotfix/1.7.2. In my opinion is this the same procedure like you would fix bugs in the main branch.

HHobeck avatar Apr 30 '22 09:04 HHobeck

I have taken a look in to the source code of GitVersion in version 5.10.1 and cannot find any usage of the TrackMergeTarget settings. In my opinion this property is not used and should be marked as deprecated.

@asbjornu: Do you know if I have missed something?

HHobeck avatar Apr 30 '22 10:04 HHobeck

Hm, good question, @HHobeck. I'm not using that feature myself, did not implement it, and am not sure exactly how it's supposed to work. It may be that we didn't have tests covering that feature and the affected code has just been refactored away. Perhaps the best thing to do is to deprecate it entirely in v6.

asbjornu avatar Aug 25 '22 21:08 asbjornu

I have found the following logic changed by Jake Ginnivan in 2015 (see commit [1]):

    public class GitFlowDevelopBranchBaseVersionStrategy : HighestTagBaseVersionStrategy
    {
        protected override bool IsValidTag(GitVersionContext context, string branchName, Tag tag, Commit commit)
        {
            if (!string.IsNullOrWhiteSpace(branchName))
            {
                if (context.Configuration.TrackMergeTarget)
                {
                    return IsDirectMergeFromCommit(tag, commit);
                }
            }

            return base.IsValidTag(context, branchName, tag, commit);
        }

        static bool IsDirectMergeFromCommit(Tag tag, Commit commit)
        {
            var targetCommit = tag.Target as Commit;
            if (targetCommit != null)
            {
                var parents = targetCommit.Parents;
                if (parents != null)
                {
                    return parents
                        .Where(parent => parent != null)
                        .Any(parent => string.Equals(parent.Id.Sha, commit.Id.Sha, StringComparison.OrdinalIgnoreCase));
                }
            }

            return false;
        }
    }

The configuration properties TrackMergeTarget was used to determine base versions in the scenario where a branch has been merged into the current branch or vice versa. In the latest code base I cannot find any usage.

commit [1]:

Jake Ginnivan Commit 1d1b0c43f9e34104af6caecca94475c54c950c2e Author: Jake Ginnivan [email protected] Date: Sunday, 15 March 2015 12:43 Parent: 1ee64bfc

Switched to configuration option rather than hardcoding branch name

HHobeck avatar Sep 09 '22 05:09 HHobeck

I would like to understand the intention of the TrackMergeTarget property. @asbjornu May be you can give me your opinion about the following scenario:

    [Test]
    public void __Just_A_Test__()
    {
        var configuration = new Config()
        {
            VersioningMode = VersioningMode.ContinuousDeployment,
            Branches = new Dictionary<string, BranchConfig>()
            {
                {
                    "develop", new BranchConfig() {
                        TrackMergeTarget = true
                    }
                }
            }
        };

        using var fixture = new EmptyRepositoryFixture();
        fixture.MakeACommit();
        fixture.AssertFullSemver("0.1.0-ci.0", configuration);
        fixture.BranchTo("develop");
        fixture.AssertFullSemver("0.1.0-alpha.0", configuration);
        fixture.MakeACommit();
        fixture.AssertFullSemver("0.1.0-alpha.1", configuration);
        fixture.BranchTo("release/1.0.0");
        fixture.AssertFullSemver("1.0.0-beta.0", configuration);
        fixture.MakeACommit();
        fixture.AssertFullSemver("1.0.0-beta.1", configuration);
        fixture.MakeACommit();
        fixture.AssertFullSemver("1.0.0-beta.2", configuration);
        fixture.Checkout("develop");
        fixture.AssertFullSemver("0.1.0-alpha.1", configuration); // 1.1.0 expected
        fixture.MakeACommit();
        fixture.AssertFullSemver("1.1.0-alpha.1", configuration);
        fixture.MergeNoFF("release/1.0.0");
        fixture.AssertFullSemver("1.1.0-alpha.4", configuration);
        fixture.Repository.Branches.Remove("release/1.0.0");
        fixture.AssertFullSemver("1.1.0-alpha.4", configuration); // okay because TrackMergeTarget=true ??
        configuration.Branches["develop"].TrackMergeTarget = false;
        fixture.AssertFullSemver("1.1.0-alpha.4", configuration); // 0.1.0 expected because TrackMergeTarget=false ??
    }

From the release management point of view this means the following: You create a release 1.0.0 branch and want to stabilize the release candidate to bring this one to production. So what happened if your product manager says to cancel the release and go back to the develop phase? The release candidate 1.0.0 has not been merged and tagged on master. But it will be properly merged to develop and deleted. Thus the next version on the develop branch is still the next version of the previous version (that means in my example the version 0.1.0).

Maybe it is a good idea to reuse this property and make the following change:

/// <summary>
/// Version is extracted from older commits' merge messages.
/// BaseVersionSource is the commit where the message was found.
/// Increments if PreventIncrementForMergedBranchVersion (from the branch config) is false.
/// </summary>
public class MergeMessageVersionStrategy : VersionStrategyBase
{
    private readonly ILog log;

    public MergeMessageVersionStrategy(ILog log, Lazy<GitVersionContext> versionContext) : base(versionContext) => this.log = log.NotNull();

    public override IEnumerable<BaseVersion> GetVersions()
    {
        if (Context.CurrentBranch.Commits == null || Context.CurrentCommit == null)
            return Enumerable.Empty<BaseVersion>();

        // new
        if (!Context.Configuration.TrackMergeTarget)
            return Enumerable.Empty<BaseVersion>();
        ...

HHobeck avatar Sep 09 '22 05:09 HHobeck

I'm also thinking about it in which circumstances the MergeMessageVersionStrategy is used in the gitflow worflow. Cannot find any scenario. Maybe it is good for the github workflow with continues delivery?

HHobeck avatar Sep 09 '22 07:09 HHobeck

The release candidate 1.0.0 has not been merged and tagged on master. But it will be properly merged to develop and deleted.

Why would a release/* branch be merged to develop but not to master? Shouldn't those merges be done at the same time and either both or none of the merges happen? I know there's no way to enforce that but socially, but still – that's how I understand the workings of Git Flow's release/* branches.

asbjornu avatar Sep 12 '22 11:09 asbjornu

I'm also thinking about it in which circumstances the MergeMessageVersionStrategy is used in the gitflow worflow. Cannot find any scenario. Maybe it is good for the github workflow with continues delivery?

I would recommend against its use for any other workflow than trunk based development aka mode: Mainline.

asbjornu avatar Sep 12 '22 11:09 asbjornu

Why would a release/* branch be merged to develop but not to master? Shouldn't those merges be done at the same time and either both or none of the merges happen? I know there's no way to enforce that but socially, but still – that's how I understand the workings of Git Flow's release/* branches.

I think it is quite common to merge from release to develop and not to master (at least not at the same time). Think about it if you finalize or stabilize a beta version and find issues. You will fix it on release and merge it back to the develop branch.

Another scenario could be if you cancel the hole release or you decide to go back to the alpha phase. Please see my comment here

HHobeck avatar Sep 12 '22 15:09 HHobeck

I would recommend against its use for any other workflow than trunk based development aka mode: Mainline.

Okay and why is the TrackMergeTarget property be true per default in the develop branch configuration? (Please see ConfigurationBuilder.cs)

HHobeck avatar Sep 12 '22 15:09 HHobeck

Okay and why is the TrackMergeTarget property be true per default in the develop branch configuration? (Please see ConfigurationBuilder.cs)

I don't see how MergeMessageVersionStrategy is related to TrackMergeTarget?

asbjornu avatar Sep 12 '22 15:09 asbjornu

I think it is quite common to merge from release to develop and not to master (at least not at the same time). Think about it if you finalize or stabilize a beta version and find issues. You will fix it on release and merge it back to the develop branch.

I suppose that's a valid use-case, but it's going to be strange selective merges because I wouldn't want everything in a release/* to be merged into develop until the release is finalized, such as the changelog, version number increments, etc. How would you resolve this?

Another scenario could be if you cancel the hole release or you decide to go back to the alpha phase.

As long as the release/* branch is not merged to main, I would just delete the release/* branch entirely. I'm not a fan of having protected release/* branches.

asbjornu avatar Sep 12 '22 15:09 asbjornu

I think it is quite common to merge from release to develop and not to master (at least not at the same time). Think about it if you finalize or stabilize a beta version and find issues. You will fix it on release and merge it back to the develop branch.

I suppose that's a valid use-case, but it's going to be strange selective merges because I wouldn't want everything in a release/* to be merged into develop until the release is finalized, such as the changelog, version number increments, etc. How would you resolve this?

What do you mean with the changelogs, version number increments, etc.? I cannot see any problems with that. A merge from release to develop is just a normal commit:

    [Test]
    public void __Just_A_Test__()
    {
        var config = new Config() { NextVersion = "1.0.0" };
        using EmptyRepositoryFixture fixture = new("develop"); // starting with develop branch

        fixture.MakeACommit();
        fixture.GetVersion(config).FullSemVer.ShouldBe("1.0.0-alpha.1");
        fixture.BranchTo("release/1.0.0");
        fixture.GetVersion(config).FullSemVer.ShouldBe("1.0.0-beta.1+0");
        fixture.MakeACommit(); // <<-- first
        fixture.GetVersion(config).FullSemVer.ShouldBe("1.0.0-beta.1+1");
        fixture.MakeACommit(); // <<-- second
        fixture.GetVersion(config).FullSemVer.ShouldBe("1.0.0-beta.1+2");
        fixture.Checkout("develop");
        fixture.GetVersion(config).FullSemVer.ShouldBe("1.1.0-alpha.0");
        fixture.MakeACommit();
        fixture.GetVersion(config).FullSemVer.ShouldBe("1.1.0-alpha.1");
        fixture.MergeNoFF("release/1.0.0"); // <<-- third
        fixture.GetVersion(config).FullSemVer.ShouldBe("1.1.0-alpha.4"); // +3 one merge and two commits from release
    }

HHobeck avatar Sep 12 '22 16:09 HHobeck

Another scenario could be if you cancel the hole release or you decide to go back to the alpha phase.

As long as the release/* branch is not merged to main, I would just delete the release/* branch entirely. I'm not a fan of having protected release/* branches.

Yep I agree with you. But when this scenario happens GitVersion should also know how to handle this.

HHobeck avatar Sep 12 '22 16:09 HHobeck

Okay and why is the TrackMergeTarget property be true per default in the develop branch configuration? (Please see ConfigurationBuilder.cs)

I don't see how MergeMessageVersionStrategy is related to TrackMergeTarget?

Yep that's the big question actually. ;) If you ask me MergeMessageVersionStrategy is the logic which should be called only when the TrackMergeTarget is set to true and this is only good for the main line mode.

HHobeck avatar Sep 12 '22 16:09 HHobeck

Okay and why is the TrackMergeTarget property be true per default in the develop branch configuration? (Please see ConfigurationBuilder.cs)

I don't see how MergeMessageVersionStrategy is related to TrackMergeTarget?

Yep that's the big question actually. ;) If you ask me MergeMessageVersionStrategy is the logic which should be called only when the TrackMergeTarget is set to true and this is only good for the main line mode.

Here is an example which illustrates the behavior:

    [Test]
    public void __Just_A_Test__()
    {
        var config = new Config() { NextVersion = "1.0.0" };
        var configWithoutTrackMergeTarget = new Config()
        {
            NextVersion = "1.0.0",
            Branches = new Dictionary<string, BranchConfig>()
            {
                { "develop", new BranchConfig() { TrackMergeTarget = false } }
            }
        };

        using EmptyRepositoryFixture fixture = new("develop"); // starting with develop

        fixture.MakeACommit();
        fixture.GetVersion(config).FullSemVer.ShouldBe("1.0.0-alpha.1");
        fixture.BranchTo("release/1.0.0");
        fixture.GetVersion(config).FullSemVer.ShouldBe("1.0.0-beta.1+0");
        fixture.MakeACommit();
        fixture.GetVersion(config).FullSemVer.ShouldBe("1.0.0-beta.1+1");
        fixture.MakeACommit();
        fixture.GetVersion(config).FullSemVer.ShouldBe("1.0.0-beta.1+2");
        fixture.Checkout("develop");
        fixture.GetVersion(config).FullSemVer.ShouldBe("1.1.0-alpha.0");
        fixture.MakeACommit();
        fixture.GetVersion(config).FullSemVer.ShouldBe("1.1.0-alpha.1");
        fixture.MergeNoFF("release/1.0.0");

        fixture.Repository.Branches.Remove("release/1.0.0");

        fixture.GetVersion(config).FullSemVer.ShouldBe("1.1.0-alpha.4");
        fixture.GetVersion(configWithoutTrackMergeTarget).FullSemVer.ShouldBe("1.0.0-alpha.5");

    }

HHobeck avatar Sep 12 '22 16:09 HHobeck

Here is a better example:

    [Test]
    public void __Just_A_Test__()
    {
        var config = new Config()
        {
            VersioningMode = VersioningMode.ContinuousDelivery,
            Branches = new Dictionary<string, BranchConfig>()
            {
                { "main", new BranchConfig() { TrackMergeTarget = true } }
            }
        };

        var configWithoutTrackMergeTarget = new Config()
        {
            VersioningMode = VersioningMode.ContinuousDelivery,
            Branches = new Dictionary<string, BranchConfig>()
            {
                { "main", new BranchConfig() { TrackMergeTarget = false } }
            }
        };

        using EmptyRepositoryFixture fixture = new("main"); // starting with main

        fixture.MakeACommit();
        fixture.ApplyTag("1.0.0");
        fixture.GetVersion(config).FullSemVer.ShouldBe("1.0.0");
        fixture.BranchTo("release/2.0.0");
        fixture.GetVersion(config).FullSemVer.ShouldBe("2.0.0-beta.1+0");
        fixture.MakeACommit();
        fixture.GetVersion(config).FullSemVer.ShouldBe("2.0.0-beta.1+1");
        fixture.MakeACommit();
        fixture.GetVersion(config).FullSemVer.ShouldBe("2.0.0-beta.1+2");
        fixture.Checkout("main");
        fixture.MergeNoFF("release/2.0.0");
        fixture.GetVersion(config).FullSemVer.ShouldBe("2.0.0+0");
        fixture.Repository.Branches.Remove("release/2.0.0");

        fixture.GetVersion(config).FullSemVer.ShouldBe("2.0.0+3");
        // until you are not tagging it this is a hotfix
        fixture.GetVersion(configWithoutTrackMergeTarget).FullSemVer.ShouldBe("1.0.1+3");
    }

HHobeck avatar Sep 12 '22 18:09 HHobeck

What do you mean with the changelogs, version number increments, etc.? I cannot see any problems with that. A merge from release to develop is just a normal commit:

What I mean is that in a support/* branch, you will often alter changelogs, bump version numbers in package.json files and such to match that of the release number. All of those versioned artifacts does not belong in develop before the release is complete (and in production), imho.

Yep that's the big question actually. ;) If you ask me MergeMessageVersionStrategy is the logic which should be called only when the TrackMergeTarget is set to true and this is only good for the main line mode.

I see. Yes, that's certainly a valid way to look at it. I don't think that expectation holds in GitVersion today, though. I believe MergeMessageVersionStrategy is executed in more cases than just for branches with TrackMergeTarget set to true.

asbjornu avatar Sep 12 '22 21:09 asbjornu

What I mean is that in a support/* branch, you will often alter changelogs, bump version numbers in package.json files and such to match that of the release number. All of those versioned artifacts does not belong in develop before the release is complete (and in production), imho.

Good point. I agree with you that this is even more complicated as expected. But I think still that GitVersion can also track or even not track the bump message when it comes from a merged branch. I mean GitVersion generates a minor or major version change on master but it is not ~~valid~~ correct when it is not tagged in the git flow workflow (or at least it dependents on how your deployment process is (ContinuousDelivery vs. ContinuousDeployment??)). Thus this feature is worth to implement in GitVersion because it pays in the support of git flow and other workflows. At this point I'm not sure if it is a new feature or if it is a part of the track-merge-target feature. Because the intention is maybe the same.

I see. Yes, that's certainly a valid way to look at it. I don't think that expectation holds in GitVersion today, though. I believe MergeMessageVersionStrategy is executed in more cases than just for branches with TrackMergeTarget set to true.

One thing may be an idea to separate this and make it configurable. This would be easier to implement and better for the understanding. Concrete my proposal is to separate this two features and introducing a new configuration property like track-merge-message.

Last but not least we need to either remove the feature track-merge-target or re-implement it in this way like it was build in the origin intention.

HHobeck avatar Sep 14 '22 05:09 HHobeck

But still could some one explain me for which scenario the feature track-merge-target is good for or give me a business use case please?

track-merge-target Strategy which will look for tagged merge commits directly off the current branch. For example develop → release/1.0.0 → merge into master and tag 1.0.0. The tag is not on develop, but develop should be version 1.0.0 now.

@Crown0815 Your example is not clear for me what you are trying to do. Which workflow applies in your example?

HHobeck avatar Sep 14 '22 05:09 HHobeck

@HHobeck we are using a custom workflow where we try to reduce dependencies between different branches and prevent "back-merges". It looks like this:

0452afbe-efad-4b48-b509-babb6a0ae15b

You can see that the support branch lives on its own and receives all the hotfixes. Those never reach master.

Simply put, master always represents versions X.Y.0, so the new features, while support always represents the X.Y.Z (with Z > 0) versions.

All I want is to be able to merge a hotfix branch into the support branch and have the bugfix version number on the hotfix branch bumped. To support this workflow, I thought that I could just configure the hotfix branches' merge target to be the support branches.

Crown0815 avatar Sep 27 '22 06:09 Crown0815

Okay thank you very much for outlining your branching and merging strategy. If I compare your custom workflow with the gitflow workflow then the difference is only how you treat the main, hotfix and support branch right!? The main branch represents the initial release (tagged as RTM) and the support branch contains the hotfix releases (also tagged as RTM of the same lifecycle). I don't understand exactly what your motivation behind it is and what advantages you have compared to the gitflow workflow.

But anyway I don't want to challenge your motivation behind it. If I read the documentation about the track-merge-changes feature then I agree this might help you detecting the version in the hotfix branch which have been done in the support branch. But I'm not sure if it is the ideal way to detect the version in all circumstances because this feature is only restricted on the current branch.

HHobeck avatar Oct 02 '22 08:10 HHobeck

In my opinion this issue here is related to the issue #1789 and vice versa and both can be solved with the track-merge-targets feature. But like I have in the other discussion already pointed out the problem might be located in the TrackReleaseBranchesVersionStrategy that this class not detecting the tagged version on branches which are marked as IsMainlin=true. The irony of my suggestion is that not only the track-merge-target feature is broken the feature detecting the tagged version on mainline branches is broken as well. Another point is that the track-merge-target feature is from the performance perspective maybe not so optimal.

TLDR: You should maybe define the support branch with IsMainline=true and implement the fix for TrackReleaseBranchesVersionStrategy detecting not only the hardcoded branch with the name master or main.

HHobeck avatar Oct 02 '22 08:10 HHobeck

:tada: This issue has been resolved in version 6.0.0-beta.1 :tada: The release is available on:

Your GitReleaseManager bot :package::rocket:

arturcic avatar Mar 02 '23 21:03 arturcic