142

I'm building a C# application, using Git as my version control.

Is there a way to automatically embed the last commit hash in the executable when I build my application?

For example, printing the commit hash to console would look something like:

class PrintCommitHash
{
    private String lastCommitHash = ?? // What do I put here?
    static void Main(string[] args)
    {
        // Display the version number:
        System.Console.WriteLine(lastCommitHash );
    }
}

Note that this has to be done at build time, not runtime, as my deployed executable will not have the git repo accessible.

A related question for C++ can be found here.

EDIT

Per @mattanja's request, I'm posting the git hook script I use in my projects. The setup:

  • The hooks are linux shell scripts, which are placed under: path_to_project\.git\hooks
  • If you are using msysgit, the hooks folder already contains some sample scripts. In order to make git call them, remove the '.sample' extension from the script name.
  • The names of the hook scripts match the event that invokes them. In my case, I modified post-commit and post-merge.
  • My AssemblyInfo.cs file is directly under the project path (same level as the .git folder). It contains 23 lines, and I use git to generate the 24th.

As my linux-shelling a bit rusty, the script simply reads the first 23-lines of AssemblyInfo.cs to a temporary file, echos the git hash to the last line, and renames the file back to AssemblyInfo.cs. I'm sure there are better ways of doing this:

#!/bin/sh
cmt=$(git rev-list --max-count=1 HEAD)
head -23 AssemblyInfo.cs > AssemblyInfo.cs.tmp
echo [assembly: AssemblyFileVersion\(\"$cmt\"\)] >> AssemblyInfo.cs.tmp
mv AssemblyInfo.cs.tmp AssemblyInfo.cs
Arialdo Martini
  • 4,427
  • 3
  • 31
  • 42
bavaza
  • 10,319
  • 10
  • 64
  • 103

20 Answers20

104

UPDATE:

Things have evolved since I originally answered this question. The Microsoft.NET.Sdk (meaning you must be using an sdk-style project) now includes support for adding the commit hash to both the assembly informational version as well as to the nuget package metadata, if some conditions are met:

  1. The <SourceRevisionId> property must be defined. This can be done by adding a target like this:
<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
    <Exec 
      Command="git describe --long --always --dirty --exclude=* --abbrev=8"
      ConsoleToMSBuild="True"
      IgnoreExitCode="False"
      >
      <Output PropertyName="SourceRevisionId" TaskParameter="ConsoleOutput"/>
    </Exec>
  </Target>

This target executes a command that will set SourceRevisionId to be the abbreviated (8 character) hash. The BeforeTargets causes this to be run before the assembly informational version is created.

  1. To include the hash in the nuget package metadata, the <RepositoryUrl> must also be defined.

  2. <SourceControlInformationFeatureSupported> property must be true, this causes the nuget pack task to pick up the SourceRevisionId as well.

I would steer people away from using the MSBuildGitHash package, since this new technique is cleaner and most consistent.

ORIGINAL:

I've created a simple nuget package that you can include in your project which will take care of this for you: https://www.nuget.org/packages/MSBuildGitHash/

This nuget package implements a "pure" MSBuild solution. If you'd rather not depend on a nuget package you can simply copy these Targets into your csproj file and it should include the git hash as a custom assembly attribute:

<Target Name="GetGitHash" BeforeTargets="WriteGitHash" Condition="'$(BuildHash)' == ''">
  <PropertyGroup>
    <!-- temp file for the git version (lives in "obj" folder)-->
    <VerFile>$(IntermediateOutputPath)gitver</VerFile>
  </PropertyGroup>

  <!-- write the hash to the temp file.-->
  <Exec Command="git -C $(ProjectDir) describe --long --always --dirty &gt; $(VerFile)" />

  <!-- read the version into the GitVersion itemGroup-->
  <ReadLinesFromFile File="$(VerFile)">
    <Output TaskParameter="Lines" ItemName="GitVersion" />
  </ReadLinesFromFile>
  <!-- Set the BuildHash property to contain the GitVersion, if it wasn't already set.-->
  <PropertyGroup>
    <BuildHash>@(GitVersion)</BuildHash>
  </PropertyGroup>    
</Target>

<Target Name="WriteGitHash" BeforeTargets="CoreCompile">
  <!-- names the obj/.../CustomAssemblyInfo.cs file -->
  <PropertyGroup>
    <CustomAssemblyInfoFile>$(IntermediateOutputPath)CustomAssemblyInfo.cs</CustomAssemblyInfoFile>
  </PropertyGroup>
  <!-- includes the CustomAssemblyInfo for compilation into your project -->
  <ItemGroup>
    <Compile Include="$(CustomAssemblyInfoFile)" />
  </ItemGroup>
  <!-- defines the AssemblyMetadata attribute that will be written -->
  <ItemGroup>
    <AssemblyAttributes Include="AssemblyMetadata">
      <_Parameter1>GitHash</_Parameter1>
      <_Parameter2>$(BuildHash)</_Parameter2>
    </AssemblyAttributes>
  </ItemGroup>
  <!-- writes the attribute to the customAssemblyInfo file -->
  <WriteCodeFragment Language="C#" OutputFile="$(CustomAssemblyInfoFile)" AssemblyAttributes="@(AssemblyAttributes)" />
</Target>

There are two targets here. The first one, "GetGitHash", loads the git hash into an MSBuild property named BuildHash, it only does this if BuildHash is not already defined. This allows you to pass it to MSBuild on the command line, if you prefer. You could pass it to MSBuild like so:

MSBuild.exe myproj.csproj /p:BuildHash=MYHASHVAL

The second target, "WriteGitHash", will write the hash value to a file in the temporary "obj" folder named "CustomAssemblyInfo.cs". This file will contain a line that looks like:

[assembly: AssemblyMetadata("GitHash", "MYHASHVAL")]

This CustomAssemblyInfo.cs file will be compiled into your assembly, so you can use reflection to look for the AssemblyMetadata at runtime. The following code shows how this can be done when the AssemblyInfo class is included in the same assembly.

using System.Linq;
using System.Reflection;

public static class AssemblyInfo
{
    /// <summary> Gets the git hash value from the assembly
    /// or null if it cannot be found. </summary>
    public static string GetGitHash()
    {
        var asm = typeof(AssemblyInfo).Assembly;
        var attrs = asm.GetCustomAttributes<AssemblyMetadataAttribute>();
        return attrs.FirstOrDefault(a => a.Key == "GitHash")?.Value;
    }
}

Some benefits to this design is that it doesn't touch any files in your project folder, all the mutated files are under the "obj" folder. Your project will also build identically from within Visual Studio or from the command line. It can also be easily customized for your project, and will be source controlled along with your csproj file.

riQQ
  • 9,878
  • 7
  • 49
  • 66
MarkPflug
  • 28,292
  • 8
  • 46
  • 54
  • 2
    This worked perfectly. I installed the nuget package and was able to pull out the git hash using `Assembly.GetExecutingAssembly()`, then examining the assembly `CustomAttributes `. – Gavin H Sep 13 '17 at 16:13
  • 1
    If this were my question, I'd have accepted this answer. Great stuff. – Drew Noakes Jan 09 '18 at 19:43
  • 1
    @GavinH, How did you get the `GitHash`? I can see that value exist but are there any pure method to get custom attribute by name? It seems that I have to write long where-select query on `CustomAttributes`, thanks. – Okan Kocyigit Jan 11 '18 at 08:20
  • 1
    @ocanal yes - unfortunately I couldn't find a cleaner way to do it than reading the `CustomAttributes`. For example, here is the function I use to extract the hash string: https://pastebin.com/nVKGLhJC – Gavin H Jan 11 '18 at 15:25
  • @ocanal: I've updated the answer with an example of how to read the git hash value at runtime. – MarkPflug Jan 11 '18 at 17:45
  • There is a potential problem - you can build from a dirty working tree and commit afterwards. After that if you build again or run your program the commit hash is not updated simply because everything was up-to-date, even though the commit has now changed. The problem is that there is now a hidden invariant - the executable might need to be updated even if no source files changed and only the commit changed. – sashoalm Aug 01 '18 at 11:45
  • Looks like this doesn't work with UseMerge/SingleAssemblyName. GenerateAssemblyInfoFromAssemblyAttributes: Generating AssemblyInfo. Setting [assembly: AssemblyMetadata("")] Successfully generated AssemblyInfo file. C:\src\packages\Microsoft.Net.Compilers.2.9.0\tools\csc.exe /out:obj\Debug\AssemblyInfo\AssemblyInfo.dll /target:library obj\Debug\AssemblyInfo\AssemblyInfo.cs obj\Debug\AssemblyInfo\AssemblyInfo.cs(4,12): error CS7036: There is no argument given that corresponds to the required formal parameter 'value' of 'AssemblyMetadataAttribute.AssemblyMetadataAttribute(string, string)' – danmiser Dec 12 '18 at 23:04
  • 2
    @danmiser I have no idea what "UseMerge/SingleAssemblyName" is, so I can't help you. Create an issue at https://github.com/MarkPflug/MSBuildGitHash and I might take a look at it (that's not a promise). – MarkPflug Dec 13 '18 at 00:03
  • 2
    Thanks for the modern answer - really clean way to do this. For any future readers who have the same issue I had: for me to get this to work (for the SDK project approach), I needed the `IncludeSourceRevisionInInformationalVersion` property as well, and I also had to have `SourceRevisionId` not set in the InitializeSourceControlInformation target, but in a separate target declared with `BeforeTargets="InitializeSourceControlInformation"`. – glen3b Aug 14 '20 at 02:17
  • 2
    For those who are like me struggling to apply the new (updated) answer: you'll need to add the part with `Target` to csproj or `Directory.Build.targets`. Also for some reason the name `InitializeSourceControlInformation` didn't work for me initially. Also you can add to `Target` tag a `Condition` attribute, i. e. to limit this target only for non-Debug configurations `Condition="'$(ConfigurationName)' != 'Debug'"` – montonero Aug 24 '20 at 07:59
  • I wanted to confirm @glen3b's findings - though I did not need the `IncludeSourceRevisionInInformationalVersion` property defined, I definitely needed `BeforeTargets="InitializeSourceControlInformation"` instead of `BeforeTargets="AddSourceRevisionToInformationalVersion"` to get this to work. – johnnyRose Oct 16 '20 at 18:45
  • 5
    Here is a working example: 0.0.2.0 $(Version)$(SourceRevisionId) – user2849936 Nov 09 '20 at 15:39
  • For me the updated variant always produces a result with a version prefix before the git hash (e.g. `0.1.0.0+abcdefgh`). Is there a way to adjust this to only get the git hash in the informational version? – display-name Jul 11 '22 at 13:18
  • Is there a way to show the `GitHash` AssemblyMetadata Value in the properties dialog from Windows Explorer? When right clicking on an assembly.dll (exe) and then Properties -> tab Details? – KargWare Jan 13 '23 at 12:09
  • Same question here. How can I show GitHash on properties of the dll or the exe file? With the old NuGet package the information are here, but now I cannot find it. – Sakura Kinomoto Feb 07 '23 at 02:46
  • For some reason this makes omnisharp fail to load the project for intellisense (in vscode): `Error: The command "git describe --long --always --dirty --exclude=* --abbrev=8" exited with code 9009`. I think this is related to this issue: https://github.com/OmniSharp/omnisharp-vscode/issues/4064 – chanban Feb 14 '23 at 20:46
91

You can embed a version.txt file into the executable and then read the version.txt out of the executable. To create the version.txt file, use git describe --long

Here are the steps:

Use a Build Event to call git

  • Right-click on the project and select Properties

  • In Build Events, add Pre-Build event containing (notice the quotes):

    "C:\Program Files\Git\bin\git.exe" describe --long > "$(ProjectDir)\version.txt"

    That will create a version.txt file in your project directory.

Embed the version.txt in the executable

  • Right click on the project and select Add Existing Item
  • Add the version.txt file (change the file chooser filter to let you see All Files)
  • After version.txt is added, right-click on it in the Solution Explorer and select Properties
  • Change the Build Action to Embedded Resource
  • Change Copy to Output Directory to Copy Always
  • Add version.txt to your .gitignore file

Read the embedded text file version string

Here's some sample code to read the embedded text file version string:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Reflection;

namespace TryGitDescribe
{
    class Program
    {
        static void Main(string[] args)
        {
            string gitVersion= String.Empty;
            using (Stream stream = Assembly.GetExecutingAssembly()
                    .GetManifestResourceStream("TryGitDescribe." + "version.txt"))
            using (StreamReader reader = new StreamReader(stream))
            {
                gitVersion= reader.ReadToEnd();
            }

            Console.WriteLine("Version: {0}", gitVersion);
            Console.WriteLine("Hit any key to continue");
            Console.ReadKey();
        }
    }
}
John Jesus
  • 2,284
  • 16
  • 18
  • 12
    This approach works reasonably well. I used "git rev-parse --short HEAD", though. – Brian Reiter Mar 31 '14 at 08:50
  • 3
    Ah, good. I used "git describe" because it is really interesting (to me) when you have a tag; the version info has the tag plus the number of commits after the tag was applied; never saw something like in an SCM before. – John Jesus Apr 10 '14 at 23:15
  • 8
    I use `git describe --dirty`, which adds a flag when developers are working with a dirty working tree. – paulmelnikow Jun 13 '14 at 17:06
  • 1
    What is the purpose of the `"TryGitDescribe."` part? – Tamás Szelei Mar 09 '16 at 10:29
  • 2
    @TamásSzelei the project namespace is TryGitDescribe. After the version.txt file gets embedded into the executable/assembly artifact, you need to prepend the namespace to get it out. – John Jesus Mar 09 '16 at 18:54
  • If you don't want an extra file lying around, you can set the `version.txt` file to be an embedded resource (you can in MonoDevelop anyway). – starbeamrainbowlabs May 19 '16 at 19:52
  • 1
    This does not play well on different platforms. What if your system shell is not the windows command line? You should be using pure MSBuild functionality and not invoking a system shell. – marknuzz Oct 21 '17 at 22:58
  • 4
    Thank you for complete solution. In my case I used `GetEntryAssembly` to get assembly. In any case you can call `GetName().Name` to avoid hardcoding the name. – astrowalker May 31 '19 at 10:53
  • 1
    rev-parse --short HEAD worked for me instead of the given describe --long, which gives me an error in visual studio (128). – mang Apr 21 '22 at 04:10
  • There are two things I don't understand there: 1) Why do you need to change Copy To Output to Always? It should be 'Never'. 2) Chicken n Egg problem. If you add the file to .gitignore then it will not be available when somebody else clones the repo but the project will expect it to be there. So the project will not build because a file is missing. Therefore... build will stop the file will not be created. – papadi Oct 16 '22 at 06:17
  • @papadi Your questions are actually related to each other. Your Question #2 will never happen because the Pre-Build Step with Copy to Output > Always insures that the version info file will be there during the build. The version info is provided when the local developer builds so that if he switches branches or make additional local commits the version will be updated, and, if using the --dirty flag as suggested by paulmelkinov above if the developer changes a file in the working tree, that will also be reflected in the version text. – John Jesus Oct 22 '22 at 16:00
70

We use tags in git to track versions.

git tag -a v13.3.1 -m "version 13.3.1"

You can get the version with hash from git via:

git describe --long

Our build process puts the git hash in the AssemblyInformationalVersion attribute of the AssemblyInfo.cs file:

[assembly: AssemblyInformationalVersion("13.3.1.74-g5224f3b")]

Once you compile, you can view the version from windows explorer:

enter image description here

You can also get it programmatically via:

var build = ((AssemblyInformationalVersionAttribute)Assembly
  .GetAssembly(typeof(YOURTYPE))
  .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false)[0])
  .InformationalVersion;

where YOURTYPE is any Type in the Assembly that has the AssemblyInformationalVersion attribute.

Handcraftsman
  • 6,863
  • 2
  • 40
  • 33
  • 17
    Hi, I wanted to ask a month ago, but, I didn't have enough rep to comment. When you say, "Our build process puts the git hash in the AssemblyInformationalVersion attribute of the AssemblyInfo.cs", what exactly is going on there? Are you just doing a visual studio build, or, are you using like something like NAnt or some other tool? – John Jesus Mar 30 '13 at 01:25
  • 3
    We use ruby (rake) to automate our build. One of our rake build tasks updates the CommonAssemblyInfo.cs file that is used across all projects in the solution. The task generates the CommonAssemblyInfo.cs file using albacore - https://github.com/derickbailey/Albacore One of the AssemblyInfo values that task sets is the AssemblyInformationalVersion. – Handcraftsman Mar 30 '13 at 01:40
  • 3
    @John Jesus - as Lazy Badger suggested, you can also use git hooks to change AssemblyInfo.cs after commit/merge etc (this is what I ended up doing). See https://www.kernel.org/pub/software/scm/git/docs/githooks.html – bavaza Aug 20 '13 at 07:38
  • Just FYI, Albacore has moved to a new hub organization: https://github.com/Albacore/albacore – kornman00 Feb 04 '14 at 08:05
  • 5
    Following project [https://github.com/jeromerg/NGitVersion](https://github.com/jeromerg/NGitVersion) offers a complete solution to generate `GlobalAssemblyInfo.*` files at compile time for C# and C++ projects: By default, the generated assembly version contains: the commit hash, a flag signaling local changes, and an increment counting the amount of commit from the repository root to the current commit. – jeromerg Feb 01 '15 at 20:11
  • As for me this cookbook is completed with prebuild events in *.csproj for libs: **git rev-parse --short HEAD >.rev; set /P REV=.rev; del .rev; echo [assembly: System.Reflection.AssemblyInformationalVersion($(VERSION)-g%REV%)] > ver.c** – mdn Jan 24 '20 at 11:23
17

I think this question is worth giving a complete step by step answer. The strategy here to is run a powershell script from the pre-build events that takes in a template file and generates an AssemblyInfo.cs file with the git tag + commit count information included.

Step 1: make an AssemblyInfo_template.cs file in the Project\Properties folder, based on your original AssemblyInfo.cs but containing:

[assembly: AssemblyVersion("$FILEVERSION$")]
[assembly: AssemblyFileVersion("$FILEVERSION$")]
[assembly: AssemblyInformationalVersion("$INFOVERSION$")]

Step 2: Create a powershell script named InjectGitVersion.ps1 whose source is:

# InjectGitVersion.ps1
#
# Set the version in the projects AssemblyInfo.cs file
#


# Get version info from Git. example 1.2.3-45-g6789abc
$gitVersion = git describe --long --always;

# Parse Git version info into semantic pieces
$gitVersion -match '(.*)-(\d+)-[g](\w+)$';
$gitTag = $Matches[1];
$gitCount = $Matches[2];
$gitSHA1 = $Matches[3];

# Define file variables
$assemblyFile = $args[0] + "\Properties\AssemblyInfo.cs";
$templateFile =  $args[0] + "\Properties\AssemblyInfo_template.cs";

# Read template file, overwrite place holders with git version info
$newAssemblyContent = Get-Content $templateFile |
    %{$_ -replace '\$FILEVERSION\$', ($gitTag + "." + $gitCount) } |
    %{$_ -replace '\$INFOVERSION\$', ($gitTag + "." + $gitCount + "-" + $gitSHA1) };

# Write AssemblyInfo.cs file only if there are changes
If (-not (Test-Path $assemblyFile) -or ((Compare-Object (Get-Content $assemblyFile) $newAssemblyContent))) {
    echo "Injecting Git Version Info to AssemblyInfo.cs"
    $newAssemblyContent > $assemblyFile;       
}

Step 3: Save the InjectGitVersion.ps1 file to your solution directory in a BuildScripts folder

Step 4: Add the following line to the project's Pre-Build events

powershell -ExecutionPolicy ByPass -File  $(SolutionDir)\BuildScripts\InjectGitVersion.ps1 $(ProjectDir)

Step 5: Build your project.

Step 6: Optionally, add AssemblyInfo.cs to your git ignore file

DeepSpace101
  • 13,110
  • 9
  • 77
  • 127
Atilio Jobson
  • 469
  • 4
  • 6
  • And remember to make your git tags compatible with file versions: such as 1.2.3. If you have more complicated tags, you will have to parse out just the compatible portions – Atilio Jobson Sep 22 '16 at 20:55
  • 2
    Instead of using a template and gitignoring real `AssemblyInfo.cs` one could modify `AssemblyInfo.cs` in place, build, then git reset `AssemblyInfo.cs` to the last commited version. So in the repo there would always be `AssemblyInfo.cs`, with `$..$` substituted only for the time of build. – Kuba Wyrostek Mar 08 '17 at 09:39
  • This worked great. I ended up using `git describe --match "v[0-9]*" --long --always --dirty` to filter for certain tags (those containing a version number) and to indicate, if the working tree was clean. – packoman Aug 16 '18 at 10:50
  • You also have to modify your RegEx in the PS script: `$gitVersion -match '[v](.*)-(\d+)-[g](.+)$';` – packoman Aug 16 '18 at 11:18
14

Another way to do this is to use the NetRevisionTool with some On-Board Visual Studio magic. I will showcase this here for Visual Studio 2013 Professional Edition, but this will work with other versions as well.

So first download the NetRevisionTool. You include the NetRevisionTool.exe in your PATH or check it in into your repository and create a visual studio pre-build and a post-build action and change your AssemblyInfo.cs.

An example that would add your git-hash to your AssemblyInformationVersion would be the following: In your project settings:

enter image description here

in the AssemblyInfo.cs of your project you change/add the line:

[assembly: AssemblyInformationalVersion("1.1.{dmin:2015}.{chash:6}{!}-{branch}")]

in the shown screenshot i checked in NetRevisionTool.exe in the External/bin folder

After build, if you then right-click your binary and go to properties then you should see something like the following:

enter image description here

Hope this helps somebody out there

schmendrick
  • 471
  • 1
  • 5
  • 12
  • The commit hash for me always ends up as 00000. I thought it was because I had uncommitted changes but still the same. Any idea why? – Viktor Nov 04 '16 at 23:41
  • 3
    The problem was NetRevision was not finding my git executable. The reason is because we are using SourceTree and git comes embedded with it. The solution was to copy git.exe and libiconv-2.dll from %USERPROFILE%\AppData\Local\Atlassian\SourceTree\git_local\bin to the folder containing NetRevision.exe. I also had to modify the events like so: Pre-build event: cd $(ProjectDir)Libraries NetRevisionTool.exe /patch $(ProjectDir) Post-build event: cd $(ProjectDir)Libraries NetRevisionTool.exe /restore $(ProjectDir) – Viktor Nov 05 '16 at 00:21
  • Just for future reference, the project repo URL has changed some time ago to https://github.com/ygoe/NetRevisionTool. More info is also available on http://unclassified.software/apps/netrevisiontool. – ygoe Nov 21 '16 at 17:24
7

Here is a simple solution that works in Visual Studio 2019 and gets the git commit hash directly into the C# file. Add the following C# code to your solution:

namespace MyNameSpace
{

    [System.AttributeUsage(System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)]
    sealed class GitHashAttribute : System.Attribute
    {
        public string Hash { get; }
        public GitHashAttribute(string hsh)
        {
            this.Hash = hsh;
        }
    }
    var hash = Assembly.GetEntryAssembly().GetCustomAttribute<GitHashAttribute>().Hash;
}

Variable hash is then going to contain the desired string if you add the following lines to your .csproj file.

<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
    <Exec Command="git.exe describe --long --always --dirty --exclude='*' --abbrev=40"
          ConsoleToMSBuild="True" IgnoreExitCode="False">
        <Output PropertyName="SourceRevisionId" TaskParameter="ConsoleOutput" />
    </Exec>
</Target>

<Target Name="SetHash" AfterTargets="InitializeSourceControlInformation">
  <ItemGroup>
    <AssemblyAttribute Include="MyNameSpace.GitHashAttribute">
        <_Parameter1>$(SourceRevisionId)</_Parameter1>
    </AssemblyAttribute>
  </ItemGroup>
</Target>

Make sure than MyNameSpace in both files matches your demand. The important point here is that the ItemGroup has to be embedded into a Target with appropriate AfterTargets set.

Claas Bontus
  • 1,628
  • 1
  • 15
  • 29
  • 1
    underrated answer. does not create any additional files and can be extended for other build-time info as well. thank you! – grasbueschel Dec 07 '22 at 08:27
  • This also solved an issue I was having trying to build in a docker container which didn't like the fact that I was having to create a file to hold the git version info. Thank you. – Blake Dec 13 '22 at 20:10
  • Thank you for the nice clean answer, after spending an hour trying to get the accepted answer working with all of its revisions and updates and comments, this worked beautifully. Just a small suggestion, you don't need to write any C# code at all, instead of creating your own attribute you could use `System.Reflection.AssemblyMetadataAttribute` which is specifically designed for this. It has two parameters instead of one, `<_Parameter1>` would be the key i.e. `SourceRevisionId` and `<_Parameter2>` would be the value `$(SourceRevisionId)` – Tobias J Jun 29 '23 at 14:33
5

It's now very easy with .NET Revision Task for MSBuild and working with Visual Studio 2019.

Simply install the NuGet package Unclassified.NetRevisionTask, then configure the information you want in theAssemblyInfo.cs file as described in the GitHub documentation.

If you only want the hash of the last commit (length=8):

[assembly: AssemblyInformationalVersion("1.0-{chash:8}")]

Build your project/solution and you'll have something like this:

enter image description here

krlzlx
  • 5,752
  • 14
  • 47
  • 55
  • To configure the format in a NET.core app add the `PropertyGroup` to the _.csproj_ file like seen in the README https://github.com/ygoe/NetRevisionTask/blob/master/README.md – sc911 Jul 08 '20 at 10:13
3

As the other answer already mentions the git bit, once you have the SHA you can consider generating the AssemblyInfo.cs file of your project in a pre-build hook.

One way to do this is to create an AssemblyInfo.cs.tmpl template file, with a placeholder for your SHA in say $$GITSHA$$, e.g.

[assembly: AssemblyDescription("$$GITSHA$$")]

Your pre build hook then has to replace this placeholder and output the AssemblyInfo.cs file for the C# compiler to pick up.

To see how this can be done using SubWCRev for SVN see this answer. It shouldn't be hard to do something similar for git.

Other ways would be a "make stage" as mentioned, i.e. write an MSBuild task that does something similar. Yet another way may be to post process the DLL somehow (ildasm+ilasm say), but I think the options mentioned above are probably easiest.

Community
  • 1
  • 1
Marcus
  • 5,987
  • 3
  • 27
  • 40
  • @Wint no, don't add the generated AssemblyInfo.cs to git. If you do it will be impossible to do a non-dirty build :P – jokedst Jan 16 '15 at 09:32
  • "It shouldn't be hard to do something similar for git." What about GitWCRev from TortoiseGit? ;-) https://tortoisegit.org/docs/tortoisegit/tgit-gitwcrev.html – Tobias Knauss Jan 04 '22 at 17:31
3

For a fully automated and flexible method checkout https://github.com/Fody/Stamp. We've successfully used this for our Git projects (as well as the this version for SVN projects)

Update: This is outdated since Stamp.Fody is no longer maintained

mamuesstack
  • 1,111
  • 2
  • 16
  • 34
  • 1
    on the github page of Stamp.Fody it says: "This project is no longer maintained.". Including it in my project raised a CA0052 and CA0055 – sc911 Mar 14 '18 at 11:07
2

You can use a powershell one-liner to update all assemblyinfo files with the commit hash.

$hash = git describe --long --always;gci **/AssemblyInfo.* -recurse | foreach { $content = (gc $_) -replace "\[assembly: Guid?.*", "$&`n[assembly: AssemblyMetadata(`"commithash`", `"$hash`")]" | sc $_ }
roh85
  • 31
  • 3
2

Another way would be to generate a Version.cs file from a Pre-Build step. I explored this in a little proof-of-concept project which prints out its current commit hash.

Tha project is uploaded on https://github.com/sashoalm/GitCommitHashPrinter.

The batch code which creates the Version.cs file is this:

@echo off

echo "Writing Version.cs file..."

@rem Pushd/popd are used to temporarily cd to where the BAT file is.
pushd $(ProjectDir)

@rem Verify that the command succeeds (i.e. Git is installed and we are in the repo).
git rev-parse HEAD || exit 1

@rem Syntax for storing a command's output into a variable (see https://stackoverflow.com/a/2340018/492336).
@rem 'git rev-parse HEAD' returns the commit hash.
for /f %%i in ('git rev-parse HEAD') do set commitHash=%%i

@rem Syntax for printing multiline text to a file (see https://stackoverflow.com/a/23530712/492336).
(
echo namespace GitCommitHashPrinter
echo {
echo     class Version
echo     {
echo         public static string CommitHash { get; set; } = "%commitHash%";
echo     }
echo }
)>"Version.cs"

popd    
sashoalm
  • 75,001
  • 122
  • 434
  • 781
1
  1. I hope you know how to call external programs and intercept output at the build-time.
  2. I hope you know how to have in git's working directory ignore unversioned files.

As noted by @learath2, output of git rev-parse HEAD will give you plain hash.

If you use tags in Git-repository (and you use tags, isn't it more descriptive and readable than git rev-parse), output may be received from git describe (while also successfully used later in git checkout)

You can call rev-parse|describe in:

  • some make stage
  • in post-commit hook
  • in smudge filter, if you'll select smudge/clean filters way of implementation
Timothy
  • 469
  • 5
  • 8
Lazy Badger
  • 94,711
  • 9
  • 78
  • 110
1

I'm using a combination of the accepted answer and a small adition. I have th AutoT4 extension installed (https://marketplace.visualstudio.com/items?itemName=BennorMcCarthy.AutoT4) to re-run the templates before build.

getting version from Git

I have git -C $(ProjectDir) describe --long --always > "$(ProjectDir)git_version.txt" in my pre-build event in project properties. Adding git_version.txt and VersionInfo.cs to .gitignore is quite a good idea.

embedding version in metadata

I have added a VersionInfo.tt template to my project:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#@ output extension=".cs" #>

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

<#
if (File.Exists(Host.ResolvePath("git_version.txt")))
{
    Write("[assembly: AssemblyInformationalVersion(\""+ File.ReadAllText(Host.ResolvePath("git_version.txt")).Trim() + "\")]");
}else{
    Write("// version file not found in " + Host.ResolvePath("git_version.txt"));
}

#>

Now I have my git tag + hash in "ProductVersion".

Arialdo Martini
  • 4,427
  • 3
  • 31
  • 42
jelinek.01
  • 13
  • 4
1
  • Open the .csproj and add <GenerateAssemblyInfo>false</GenerateAssemblyInfo> to the first PropertyGroup
    • You may want to copy the contents of the already generated AssemblyInfo.cs in the obj folder so you don't have to write everything yourself.
  • Create AssemblyInfo.tt (T4 template) in the properties folder.
  • Paste the following contents + the old contents of your previously auto generated AssemblyInfo.cs
<#@ template debug="true" hostspecific="True" language="C#" #>
<#@ assembly name="System.Core" #>
<# /*There's a bug with VS2022 where you have to be real specific about envDTE.*/ #>
<#@ assembly name="./PublicAssemblies/envdte.dll" #>  
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Globalization" #>
<#@ output extension=".cs" #>
<#
    var dte = ((IServiceProvider)this.Host).GetService(typeof(EnvDTE.DTE)) as EnvDTE.DTE;
    string buildConfig = dte.Solution.SolutionBuild.ActiveConfiguration.Name;
    string solutionDirectory = Path.GetDirectoryName(dte.Solution.FullName);

    var (gitRevision, gitBranch, gitCompactRevision) = ("", "", "");

    using(var process = new System.Diagnostics.Process() {
        StartInfo = new System.Diagnostics.ProcessStartInfo() {
            WorkingDirectory = solutionDirectory,
            FileName = @"cmd.exe",
            Arguments = "/C git rev-parse HEAD & git rev-parse --abbrev-ref HEAD",
            RedirectStandardError = true,
            RedirectStandardOutput = true,
            UseShellExecute = false,
            CreateNoWindow = true
        }
    }) {
        process.Start();
        string[] lines = process.StandardOutput.ReadToEnd().Split();
        gitRevision = lines[0].Trim();
        gitBranch = lines[1].Trim();
        gitCompactRevision = gitRevision.Substring(0, 6);
    }
    string appPurpose         = "Launcher"; // & Updater
    string companyShort       = "todo";
    string companyFull        = "todo";
    string productNameShort   = "todo";
    string productName        = $"{companyShort} {productNameShort}";
    string fileName           = $"{companyShort}{productNameShort}";
    string exeNAME            = $"{fileName}Launch";
    string originalFilename   = $"{exeNAME}.exe";
    string CALLEXE            = $"{fileName}.exe";
    string BROWSEREXE         = $"{fileName}Browser.exe";
    string FULLINSTALLER      = $"{fileName}Setup.exe";

    DateTime dtBuiltDate      = DateTime.UtcNow;
    string cBuildYear         = dtBuiltDate.Year.ToString();
    string cBuildDay          = dtBuiltDate.ToString("dd");
    string cBuildMonth        = dtBuiltDate.ToString("MM");
    string cBuildTime         = dtBuiltDate.ToString("T", DateTimeFormatInfo.InvariantInfo);
    string assemblyVersion    = $"3.0.{cBuildYear}.{cBuildMonth}{cBuildDay}";

    string JOB_NAME           = System.Environment.GetEnvironmentVariable("JOB_NAME") ?? "0.0";
    string buildVersion       = System.Environment.GetEnvironmentVariable("BUILD_NUMBER") ?? "0-dev";
    string buildSeries        = Regex.Replace(JOB_NAME, @"[^0-9\.]+", "");
    string buildNumber        = Regex.Replace(buildVersion, @"[^0-9\.]+", "");
    string InternalVersion    = $"{JOB_NAME}.{buildVersion}";
    string fileVersion        = Regex.Replace(InternalVersion, @"[^0-9\.]+", "");
#>
using System.Reflection;

[assembly: System.Runtime.InteropServices.ComVisible(false)]
[assembly: System.Resources.NeutralResourcesLanguageAttribute("en")]
[assembly: AssemblyConfigurationAttribute("<#= buildConfig #>")]
[assembly: AssemblyProduct("<#= productName #>")]
[assembly: AssemblyTitle("<#= $"{companyShort}{productNameShort}" #>")]
[assembly: AssemblyCompany("<#= companyFull #>")]
[assembly: AssemblyDescription("<#= $"{companyShort} {productNameShort} .... {appPurpose} - ...... by {companyFull}" #>")]
[assembly: AssemblyCopyright("<#= $"© 1983-{cBuildYear} {companyFull}" #>")]
[assembly: AssemblyTrademark("<#= $"{productName} is a trademark of {companyFull}, Inc." #>")]
[assembly: AssemblyInformationalVersion("<#= InternalVersion #>")]
[assembly: AssemblyVersion("<#= assemblyVersion #>")]
[assembly: AssemblyFileVersion("<#= fileVersion #>")]
[assembly: AssemblyMetadataAttribute("OriginalFilename",    "<#= originalFilename #>")]
[assembly: AssemblyMetadataAttribute("NAME",                "<#= $"{productName} {appPurpose}" #>")]
[assembly: AssemblyMetadataAttribute("EXENAME",             "<#= exeNAME #>")]
[assembly: AssemblyMetadataAttribute("DIRNAME",             "<#= productNameShort #>")]
[assembly: AssemblyMetadataAttribute("CALLEXE",             "<#= $"{fileName}.exe" #>")]
[assembly: AssemblyMetadataAttribute("BROWSEREXE",          "<#= $"{fileName}Browser.exe" #>")]
[assembly: AssemblyMetadataAttribute("FULLINSTALLER",       "<#= $"{fileName}Setup.exe" #>")]
[assembly: AssemblyMetadataAttribute("COMPANY",             "<#= companyFull #>")]
[assembly: AssemblyMetadataAttribute("License",             "<#= $"Contains copyrighted code and applications ..." #>")]
[assembly: AssemblyMetadataAttribute("TermsOfUse",          "<#= "https://www.company.com/en-us/terms-of-use/" #>")]
[assembly: AssemblyMetadataAttribute("Website",             "<#= "https://www.company.com/en-us" #>")]
[assembly: AssemblyMetadataAttribute("UpdateURL",           "https://subdomain.product.net/version_check")]

[assembly: AssemblyMetadataAttribute("BuildYear",           "<#= cBuildYear #>")]
[assembly: AssemblyMetadataAttribute("BuildDay",            "<#= cBuildDay #>")]
[assembly: AssemblyMetadataAttribute("BuildMonth",          "<#= cBuildMonth #>")]
[assembly: AssemblyMetadataAttribute("BuildTime",           "<#= cBuildTime #>")]
[assembly: AssemblyMetadataAttribute("DateModified",        "<#= $"{dtBuiltDate.ToString("MMM dd, yyyy", DateTimeFormatInfo.InvariantInfo)} at {cBuildTime}" #>")]

[assembly: AssemblyMetadataAttribute("BuildSeries",         "<#= buildSeries #>")]
[assembly: AssemblyMetadataAttribute("BuildNumber",         "<#= buildNumber #>")]
[assembly: AssemblyMetadataAttribute("BuildDate",           "<#= dtBuiltDate.ToString("s") #>")]
[assembly: AssemblyMetadataAttribute("BuildMachine",        "<#= Environment.MachineName #>")]
[assembly: AssemblyMetadataAttribute("BuildMachineUser",    "<#= Environment.UserName #>")]
[assembly: AssemblyMetadataAttribute("BuildOSVersion",      "<#= Environment.OSVersion #>")]
[assembly: AssemblyMetadataAttribute("BuildPlatform",       "<#= Environment.OSVersion.Platform #>")]
[assembly: AssemblyMetadataAttribute("BuildClrVersion",     "<#= Environment.Version #>")]

[assembly: AssemblyMetadataAttribute("BuildBranch",         "<#= gitBranch #>")]
[assembly: AssemblyMetadataAttribute("BuildRevision",       "<#= gitCompactRevision #>")]
[assembly: AssemblyMetadataAttribute("CommitHash",          "<#= gitRevision #>")]
[assembly: AssemblyMetadataAttribute("RepositoryUrl",       "")]
[assembly: AssemblyMetadataAttribute("RepositoryType",      "")]
<#+

#>

You can now use the full power of C# to generate whatever you want, such as the git branch and revision you're currently on. Some tips:

  • Variables can be declared anywhere inside a <# #> block
  • Any methods you wish to use must be declared at the end of the file in a <#+ #> block. (The + sign is very important and it must be the last thing at the end of the file))
  • Everything outside of <# #> blocks is just plain text.
  • VS2019 has no syntax highlighting or intellisense. The .tt file is plain text. I recommend editing it with vscode after installing T4 Support extension (not available in vs2019...)
Derek Ziemba
  • 2,467
  • 22
  • 22
1

Only one line, add reference to Microsoft.SourceLink.GitHub

    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />

and git commit hash is auto imbedded to .Net dll.

For more details read docs

M.Hassan
  • 10,282
  • 5
  • 65
  • 84
0

Referring to the another answer (https://stackoverflow.com/a/44278482/4537127) i also utilised the VersionInfo.tt text template to generate AssemblyInformationalVersion without AutoT4.

(Atleast works in my C# WPF application)

Problem was that the Pre-build events were run after template transformations, so after cloning, the git_version.txt file was not there and build fails. After creating it manually to allow transformation to pass once, it was updated after transformation, and was always one commit behind.

I had to make two adjustments to the .csproj file (this applies at least for Visual Studio Community 2017)

1) Import the Text Transformation Targets and make template transformations to run on every build: (Ref https://msdn.microsoft.com/en-us/library/ee847423.aspx)

<PropertyGroup>
    <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">15.0</VisualStudioVersion>
    <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
    <TransformOnBuild>true</TransformOnBuild>
    <TransformOutOfDateOnly>false</TransformOutOfDateOnly>
</PropertyGroup>

and after <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

<Import Project="$(VSToolsPath)\TextTemplating\Microsoft.TextTemplating.targets" />

2) Make the git describe run before template transformations (so that git_version.txt is there when VersionInfo.tt is transformed) :

<Target Name="PreBuild" BeforeTargets="ExecuteTransformations">
  <Exec Command="git -C $(ProjectDir) describe --long --always --dirty &gt; $(ProjectDir)git_version.txt" />
</Target>

..And the C# code to get the AssemblyInformationalVersion (Ref https://stackoverflow.com/a/7770189/4537127)

public string AppGitHash
{
    get
    {
        AssemblyInformationalVersionAttribute attribute = (AssemblyInformationalVersionAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false).FirstOrDefault();

        return attribute.InformationalVersion;
    }
}

..And add the generated files to .gitignore

VersionInfo.cs
git_version.txt
kimmoli
  • 111
  • 1
  • 4
0

Place

<Target Name="UpdateVersion" BeforeTargets="CoreCompile">
  <Exec Command="php &quot;$(SolutionDir)build.php&quot; $(SolutionDir) &quot;$(ProjectDir)Server.csproj&quot;" />
</Target>

in YOUR_PROJECT_NAME.csproj

<?php

function between(string $string, string $after, string $before, int $offset = 0) : string{
    return substr($string, $pos = strpos($string, $after, $offset) + strlen($after),
        strpos($string, $before, $pos) - $pos);
}

$pipes = [];
$proc = proc_open("git rev-parse --short HEAD", [
    0 => ["pipe", "r"],
    1 => ["pipe", "w"],
    2 => ["pipe", "w"]
], $pipes, $argv[1]);

if(is_resource($proc)){
    $rev = stream_get_contents($pipes[1]);
    proc_close($proc);
}

$manifest = file_get_contents($argv[2]);
$version = between($manifest, "<Version>", "</Version>");
$ver = explode("-", $version)[0] . "-" . trim($rev);
file_put_contents($argv[2], str_replace($version, $ver, $manifest));

echo "New version generated: $ver" . PHP_EOL;

PeratX
  • 1
  • 1
0

Heavily inspired by @John Jesus answer, I've created a Powershell v1 script that runs on each Build to adjust the Assembly Version to the current Git tag.

The Powershell script

# Get build running directory
$scriptPath = split-path -parent $MyInvocation.MyCommand.Path
try {
    $v = git describe --tags
}
catch [System.Management.Automation.CommandNotFoundException] {
    # Git not found
    exit
}

# Letters are incompatible with AssemblyVersion.cs so we remove them
$v = $v -replace "v", ""
# Version format is major[.minor[.build[.revision]] so we remove them
$v = $v -replace "-(\D.*)", ''
$v = $v -replace "-", '.'

# We replace versions inside AssemblyInfo.cs content
$info = (Get-Content ($scriptPath + "/properties/AssemblyInfo.cs"))
$av = '[assembly: AssemblyVersion("'+$v+'")]'
$avf = '[assembly: AssemblyFileVersion("'+$v+'")]'
$info = $info -replace '\[assembly: AssemblyVersion\("(.*)"\)]', $av
$info = $info -replace '\[assembly: AssemblyFileVersion\("(.*)"\)]', $avf
Set-Content -Path ($scriptPath + "/properties/AssemblyInfo.cs") -Value $info -Encoding UTF8

Place it your solution folder and set a Prebuild Event to launch it: prebuild-event

Azerpas
  • 304
  • 2
  • 10
0

I deploy these files on our dev/staging systems to have a quick look:

git.exe -C "$(ProjectDir.TrimEnd('\'))" describe --long > "$(ProjectDir)_Version.info":

MyResult: 10.02.0.3-247-gbeeadd082

git.exe -C "$(ProjectDir.TrimEnd('\'))" branch --show-current > "$(ProjectDir)_Branch.info"

MyResult: feature/JMT-3931-jaguar

(Visual Studio PreBuild Events)

user2029101
  • 116
  • 9
0

The more convenient way I found it to add

<ItemGroup>
    <PackageReference Include="GitVersion.MsBuild" Version="5.10.3">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
</ItemGroup>

to .csproj. This builds on top of the very flexible GitVersion tool.

This sets the Product Version in the produced dll. You can control which information to be output by creating a GitVersion.yml file containing

assembly-informational-format: '{Sha}'

Other formats are available. They are listed in the Version Variables in the GitVersion documentation.

Another option is to use SourceLink, which is provided by Microsoft itself. Honestly, though, I found a great deal of quirks and limitations with this approach (e.g. it seems SourceLinks wants to know where the repo is hosted, and must configured differently depending on it is on Azure DevOps, GitHub, GitLab, BitBucket, gitea etc).

Arialdo Martini
  • 4,427
  • 3
  • 31
  • 42