Tuesday, November 28, 2006

Merging and resolving conflicts using API

Recently, there were several questions on MSDN forums how to perform merge and resolve conflicts using Version Control object model API. Insofar as online help for those API is very insufficient, I thought I'd share merge related gotchas that I have accumulated over the time.
I will try to have a look at somewhat typical scenario of merge automation (where all changes are merged with uniform conflict resolution algorithm).

First thing to do is to call Merge method of Workspace class:


    Workspace workspace = _serverVc.GetWorkspace("WORKSTATION",
                "DOMAIN\\user");
    GetStatus status = workspace.Merge("$/Project/Ongoing/Solution",
                        "$/Project/Branches/Solution",
                        null,
                        null,
                        LockLevel.None,
                        RecursionType.Full,
                        MergeOptions.None);

Let’s have a look at Merge and its parameters (I will talk about more complex overloaded version; other version just takes default values for unexposed parameters):

    public GetStatus Merge (string sourcePath,
                        string targetPath,
                        VersionSpec versionFrom,
                        VersionSpec versionTo,
                        LockLevel lockLevel,
                        RecursionType recursion,
                        MergeOptions mergeOptions)

First two parameters specify source path and target path for the merge. Those may be either server or local paths (the target path must be mapped in workspace prior to merge).
VersionFrom” and “versionTo” parameters may be used to specify range of versions to perform merge from. One may use that in several different ways. By default (if null is specified for first parameter, null or latest version for second), all unmerged changes in source will be merged. If same changeset is specified in both parameters, only changes in that changeset will be merged (selective merge). To merge changes up to specific changeset, first parameter may be null and second version up to which merge required.
LockLevel” parameter specifies what lock will be set on changes pended (LockLevel.None will usually do – I cannot at the moment think of any scenario you would like other lock).
Recursion” parameter specifies level of recursion to use on source and target paths (usually with folders one will use RecursionType.Full).
MergeOptions” parameter is the one that defines merge initial behavior. It may have the following values (see MergeOptions enum):

  • None – no special options (same as using Merge wizard in VS UI)

  • AlwaysAcceptMine – discard any changes in source, and just update merge history (same as discard option in tf.exe merge command, not available in VS UI). Essentially, the option says to resolve all conflicts using Resolution.AlwaysAcceptYours (more on that below)

  • ForceMerge – do not look at merge history and perform merge for specified range of versions from source as if no merges were performed (same as force option in tf.exe merge command, not available in VS UI). When that option is specified, “versionFrom” and “versionTo” parameters must be set in call to Merge

  • Baseless – perform baseless merge (when source and target items have no branch relationship between them)

  • NoMerge – do not perform actual merge (same as preview option in tf.exe merge command, not available in VS UI)

All options except NoMerge are mutually exclusive.

After all parameters values were specified and Merge was invoked, the next thing to do is to look at returned value (of type GetStatus). Personally, I dislike it very much as it provides information in rather incomprehensible way - each field in return value as well as their combination tell you what happens in merge.

Possibilities are (I know those by trial and error, so probably there is still dearth of other interesting combinations):

  • NoActionNeeded == true && NumOperations == 0  – means that no changes in source needed to be merged, so no actual changes were pended

  • NoActionNeeded == false && NumOperations > 0 && HaveResolvableWarnings == false  – means that merges were performed, but all conflicts were resolved automatically. Need to check in pended merge changes and that’s about it

  • NoActionNeeded == false && NumConflicts > 0  – merge was performed and there are conflicts to resolve

First two cases are obvious. In the last case there are conflicts and resolution is required. I will talk only about simple conflicts (content changes) and not rename/delete changes (those are kinda complicated, and I will leave that to MS guys with access to source code; besides I doubt if there is any merit in automatic merge or conflict resolution for delete/rename changes).

Let’s try to implement conflict resolution algorithm similar to manual merge in Visual Studio.

First, one needs to retrieve list of conflicts:

Conflict[] conflicts = workspace.QueryConflicts(new string[] { "$/Project/Branches/Solution" }, true);



The method QueryConflicts is pretty obvious – it returns all conflicts on the specified paths in the workspace, last parameter specifying whether query should be recursive.
Now it is possible to iterate over the conflicts and resolve them one by one:

    foreach (Conflict conflict in conflicts)
    {
        if (workspace.MergeContent(conflict, true))
        {
            conflict.Resolution = Resolution.AcceptMerge;
            workspace.ResolveConflict(conflict);
        }
    }

The code above calls for each conflict method MergeContent, that will invoke visual merge tool. After the user performed visual merge (known by MergeContent return value being true), the conflict is ready for resolution.
To resolve conflict, the Resolution property of the conflict is changed according to the resolution (see Resolution enum). Possible values are (I discuss only options relevant to simple merge scenarios):

  • AcceptYours – local version is to be used for merge

  • AcceptTheirs – server version is to be used for merge

  • AcceptMerge – resolve conflict by doing manual merge

Now, in my code I use AcceptMerge as it is expected that user will create version using the merge tool locally.
After the resolution is set, ResolveConflict method is called to signal that conflict is resolved (if resolution succeeded, IsResolved property of the conflict will now return true). By the way, if we talk about conflict properties already, another useful property is IsNamespaceConflict; it lets you know whether conflict is in file content or in version control namespace (rename etc.)
Suprisingly, AcceptMerge resolution option goes an extra mile for you and does something similar to code snippet below

    if (conflict.IsResolved)
    {
        workspace.PendEdit(conflict.TargetLocalItem);
        File.Copy(conflict.MergedFileName, conflict.TargetLocalItem,
            true);
    }

After the resolution, you have Edit pending on the file and merged file as a local copy.

Once all conflicts are resolved it is possible to check in changed files and thus complete merge.

But resolution of conflicts raises several additional issues (even if not taking into account renames and deletes). For example, if during conflict resolution one specifies that source version should be taken in merge that essentially means that local file after resolution must be identical to source version. It turns out that ResolveConflict will handle those situations for you: for example, after resolution with AcceptTheirs you will have source version in your workspace without doing anything extra.

Obviously, the same steps to conflict resolution may be used when resolving conflicts that occur on check in (though I am not sure I can see ready automation scenarios there).

In conclusion, I would not recommend using Version Control API for merge and conflict resolutions but rather recommend sticking to command line tf.exe client for advanced scenarios. While the thing is doable, you should be prepared to spend quite an amount of quality time on it and be prepared later to fix bugs (mostly related to myriad scenarios and border cases you did not think of).

Please take the examples above with a grain of salt; if you find errors/omissions do let me know so I can keep it updated.

3 comments:

Anonymous said...

Is there an option to choose the changeset that you want to merge and fine the conflict?

eugenez said...

Anon,

When you call Merge, it is possible to specify the changesets range you'd be merging (version from/to parameters)

Edwin said...

Thank you, thank you, thank you.

I was searching for so long now...
Could not find anything use full. Only like, snippets of code :)

Thanx for explaining it so neatly :)