Tagging assemblies with Mercurial changeset hash


Once you move to a distributed version control system such as Git or Hg, the concept of incremental commit numbers begins to lose its meaning as exists in centralized version control systems such as SVN or TFS.  In centralized VCS, version numbers are kept as an incremental revision number.  Since only one commit can occur against the central repository at a time, it’s easy to keep track of this number.

With distributed VCS, there is no central repository.  The repository is distributed and duplicated to each local machine, with only some external repository metadata pointing to upstream repositories.

As an approximation for revision numbers, many DVCS solutions include a local incremental revision number.  However, this number is not tied to the actual commit/changeset.  Once the local commits are pushed upstream, those local revision numbers don’t travel along.  The upstream repository will have its own local revision numbers.

Alternates to versioning

A common practice in centralized VCS was to include the revision number as part of the assembly version, so the “revision” part of “Major.Minor.Build.Revision” was the actual VCS revision number.  With DVCS, this number is misleading at the least at completely wrong in general.  The revision number of the upstream repository is completely local to that repository, and has no connection to the individual remote repository’s revision numbers.  That they might be the same is possible, but shouldn’t be counted on.

The whole plan is to tag our assemblies with metadata information that allow us to tie that assembly back to the specific commit whose build produced that artifact.  Because the commit hashes don’t change, those are the best bet to pick for a cross-repository representation of what specific commit produced that assembly.

Now that we have a reliable, cross-repository method to identify a commit, the next question is where to put it.

One option is to include this information in the AssemblyDescriptionAttribute.  It doesn’t really matter, other than deciding on what your convention should be.  Anything in the System.Reflection namespace should work with the exception of the version attributes, which require a specific format.

Retrieving the hash

To get the current hash, we can use the “hg log” command, or more easily, the “hg tip” command.  We can use the styling to retrieve just the hash of the tip of the current repository:

C:devworking [default]> hg tip --template "{node}n"
3f71b6267c8c88ac0585cfa886b1bd533b5607ea

Because all modern build solutions allow us to call out to command-line tools, all we need to do now is incorporate this into our current assembly-info-generation strategy.  My NAnt solution is:

<if test="${environment::variable-exists('BUILD_NUMBER')}">
        <property name="project.fullversion" value="${environment::get-variable('BUILD_NUMBER')}" />
    </if>
    <exec program="hg" commandline="tip --template {node}" output="changeset.txt" />
    <loadfile file="changeset.txt" property="hg.changeset" />

    <echo message="MARKING THIS BUILD AS VERSION ${project.fullversion}" />
    <delete file="${dir.solution}/CommonAssemblyInfo.cs" failonerror="false"/>
    <asminfo output="${dir.solution}/CommonAssemblyInfo.cs" language="CSharp">
        <imports>
            <import namespace="System" />
            <import namespace="System.Reflection" />
            <import namespace="System.Runtime.InteropServices" />
        </imports>
        <attributes>
            <attribute type="ComVisibleAttribute" value="false" />
            <attribute type="AssemblyVersionAttribute" value="${project.fullversion}" />
            <attribute type="AssemblyFileVersionAttribute" value="${project.fullversion}" />
            <attribute type="AssemblyCopyrightAttribute" value="Copyright © ${company.name} ${datetime::get-year(datetime::now())}" />
            <attribute type="AssemblyProductAttribute" value="${project::get-name()}" />
            <attribute type="AssemblyCompanyAttribute" value="${company.name}" />
            <attribute type="AssemblyConfigurationAttribute" value="${project.config}" />
            <attribute type="AssemblyInformationalVersionAttribute" value="${project.fullversion}" />
            <attribute type="AssemblyDescriptionAttribute" value="${string::trim(hg.changeset)}" />
        </attributes>
        <references>
            <include name="System.dll" />
        </references>
    </asminfo>

It’s a bit generic.  First, I use the “exec” task to execute the “hg tip” command and place the output into a file.  I then use the “loadfile” task to read the file into a variable.  I have to jump through these hoops because NAnt doesn’t allow me to place the output of a command directly into a property.

After that, it’s just a matter of modifying my current assembly-info-generation method, in this case the “asminfo” task.  With PSake, it’s slightly different but more of the same.  The calling out to command is different, the common-assembly-info-generation method is different, but the approach is the same.

If you want to get even more interesting, for internal web application projects, I’ll include a hyperlink at the bottom of the screen showing the version, but linking directly to our remote Hg server’s commit in BitBucket.  I know exactly what is deployed without even needing to look at tags, the build server or anything.  Just click the deployed commit hash to see what’s in production.

Defining unit tests