// Copyright Keysight Technologies 2012-2019 // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, you can obtain one at http://mozilla.org/MPL/2.0/. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using LibGit2Sharp; using OpenTap.Cli; namespace OpenTap.Package { /// /// CLI sub command `tap sdk gitversion` that can calculate a version number based on the git history and a .gitversion file. /// [Display("gitversion", Group: "sdk", Description: "Calculate the semantic version number for a specific git commit.")] public class GitVersionAction : OpenTap.Cli.ICliAction { private static readonly TraceSource log = Log.CreateSource("GitVersion"); /// /// Represents the --gitlog command line argument which prints git log for the last n commits including version numbers for each commit. /// [CommandLineArgument("gitlog", Description = "Print the git log for the last commits including their semantic version number.")] public string PrintLog { get; set; } /// /// Represents an unnamed command line argument which specifies for which git ref a version should be calculated. /// [UnnamedCommandLineArgument("ref", Required = false, Description = "An optional sha ref to a specific commit. If not specified, the current HEAD is used.")] public string Sha { get; set; } /// /// Represents the --replace command line argument which causes this command to replace all occurrences of $(GitVersion) in the specified file. Cannot be used together with --gitlog. /// [CommandLineArgument("replace", Description = "Replace all occurrences of $(GitVersion) in the specified file\nwith the calculated semantic version number. It cannot be used with --gitlog.")] public string ReplaceFile { get; set; } /// /// Represents the --fields command line argument which specifies the number of version fields to print/replace. /// [CommandLineArgument("fields", Description = "Number of version fields to print/replace. The fields are: major, minor, patch,\n" + "pre-release, and build metadata. E.g., --fields=2 results in a version number\n" + "containing only the major and minor field. The default is 5 (all fields).")] public int FieldCount { get; set; } /// /// Represents the --dir command line argument which specifies the directory in which the git repository to use is located. /// [CommandLineArgument("dir", Description = "Directory containing the git repository to calculate the version number from.")] public string RepoPath { get; set; } /// /// Constructs new action with default values for arguments. /// public GitVersionAction() { RepoPath = Directory.GetCurrentDirectory(); FieldCount = 5; } /// /// Executes this action. /// /// Returns 0 to indicate success. public int Execute(CancellationToken cancellationToken) { if (FieldCount < 1 || FieldCount > 5) { log.Error("The argument for --fields ({0}) must be an integer between 1 and 5.", FieldCount); return (int)ExitCodes.ArgumentError; } if (!String.IsNullOrEmpty(PrintLog)) { int nLines = 0; if (!int.TryParse(PrintLog, out nLines) || nLines <= 0) { log.Error("The argument for --gitlog ({0}) must be an integer greater than 0.", PrintLog); return (int)ExitCodes.ArgumentError; } return DoPrintLog(cancellationToken); } string versionString = null; using (GitVersionCalulator calc = new GitVersionCalulator(RepoPath)) { try { if (String.IsNullOrEmpty(Sha)) versionString = calc.GetVersion().ToString(FieldCount); else versionString = calc.GetVersion(Sha).ToString(FieldCount); } catch (Exception ex) { if (ex.Message.Contains("object not found - no match for id")) { throw new ExitCodeException((int) ExitCodes.GeneralException, "Failed getting git version because the repository history is incomplete.\n" + "Please ensure that the repository has a full version history (git fetch --unshallow).\n" + "If this is occurring on a gitlab runner, ensure 'Git shallow clone' is set to 0."); } throw; } } if (!String.IsNullOrEmpty(ReplaceFile)) { if (!File.Exists(ReplaceFile)) { log.Error("File '{0}' given in --replace argument could not be found.", Path.GetFullPath(ReplaceFile)); return (int)ExitCodes.ArgumentError; } int replaceLineCount = DoReplaceFile(ReplaceFile, versionString); if (replaceLineCount == 0) log.Warning("Nothing to replace. '$(GitVersion)' was not found in {0}.", ReplaceFile); else log.Info("Replaced '$(GitVersion)' with '{0}' in {1} line(s) of {2}", versionString, replaceLineCount, ReplaceFile); return (int)ExitCodes.Success; } log.Info(versionString); return (int)ExitCodes.Success; } private static int DoReplaceFile(string fileName, string versionString) { int replaceLineCount = 0; using (var input = File.OpenText(fileName)) using (var output = new StreamWriter(fileName + ".tmp")) { string line; while (null != (line = input.ReadLine())) { if (line.Contains("$(GitVersion)")) { replaceLineCount++; line = line.Replace("$(GitVersion)", versionString); } if (line.Contains("$(GitLongVersion)")) { replaceLineCount++; line = line.Replace("$(GitLongVersion)", versionString); } output.WriteLine(line); } } //File.Replace(fileName + ".tmp", fileName, fileName + ".org"); File.Replace(fileName + ".tmp", fileName, null); return replaceLineCount; } private int DoPrintLog(CancellationToken cancellationToken) { ConsoleColor defaultColor = Console.ForegroundColor; ConsoleColor graphColor = ConsoleColor.DarkYellow; ConsoleColor versionColor = ConsoleColor.DarkRed; using (GitVersionCalulator versionCalculater = new GitVersionCalulator(RepoPath)) using (LibGit2Sharp.Repository repo = new LibGit2Sharp.Repository(RepoPath)) { Commit tip = repo.Head.Tip; if (!string.IsNullOrEmpty(Sha)) { tip = repo.Lookup(Sha); if(tip == null) { log.Error($"The commit with reference {Sha} does not exist in the repository."); return (int)ExitCodes.ArgumentError; } } IEnumerable History = repo.Commits.QueryBy(new CommitFilter() { IncludeReachableFrom = tip, SortBy = CommitSortStrategies.Topological }); // Run through to determine max Position (number of concurrent branches) to be able to indent correctly later int maxLines = int.Parse(PrintLog); int lineCount = 0; Dictionary commitPosition = new Dictionary(); commitPosition.Add(History.First(), 0); int maxPosition = 0; foreach (Commit c in History) { cancellationToken.ThrowIfCancellationRequested(); if (maxPosition < commitPosition[c]) maxPosition = commitPosition[c]; if(!c.Parents.Any()) { // this is the very first commit in the repo. Stop here. maxLines = ++lineCount; break; } Commit p1 = c.Parents.First(); if (c.Parents.Count() > 1) { Commit p2 = c.Parents.Last(); if (commitPosition.ContainsKey(p1)) if (commitPosition[p1] != commitPosition[c]) { int startPos = Math.Min(commitPosition[p1], commitPosition[c]); int endPos = Math.Max(commitPosition[p1], commitPosition[c]); commitPosition[c] = startPos; foreach (var kvp in commitPosition.Where((KeyValuePair kvp) => kvp.Value == endPos).ToList()) { commitPosition.Remove(kvp.Key); } } if (!commitPosition.ContainsKey(p2)) { // move out to an position out for the new branch int newPosition = commitPosition[c] + 1; while (commitPosition.ContainsValue(newPosition) && (newPosition <= commitPosition.Values.Max())) newPosition++; commitPosition[p2] = newPosition; commitPosition[p1] = commitPosition[c]; } else if (!commitPosition.ContainsKey(p1)) { commitPosition[p1] = commitPosition[c]; } } else { if (!commitPosition.ContainsKey(p1)) commitPosition[p1] = commitPosition[c]; if (commitPosition[p1] != commitPosition[c]) { int startPos = Math.Min(commitPosition[p1], commitPosition[c]); int endPos = Math.Max(commitPosition[p1], commitPosition[c]); // c is now merged back, no need to keep track of it (or any other commit on this branch) // this way we can reuse the position for another branch foreach (var kvp in commitPosition.Where((KeyValuePair kvp) => kvp.Value == endPos).ToList()) { commitPosition.Remove(kvp.Key); } commitPosition[p1] = startPos; foreach (var kvp in commitPosition.Where((KeyValuePair kvp) => kvp.Value == startPos).ToList()) { if(kvp.Key != p1) commitPosition.Remove(kvp.Key); } } } if (++lineCount >= maxLines) break; } { maxPosition++; } { // Run through again to print lineCount = 0; commitPosition = new Dictionary(); commitPosition.Add(History.First(), 0); HashSet taggedCommits = repo.Tags.Select(t => t.Target.Peel()).ToHashSet(); foreach (Commit c in History) { cancellationToken.ThrowIfCancellationRequested(); void DrawPositionSpacer(int fromPos,int toPos) { for (int i = fromPos; i < toPos; i++) { if(commitPosition.ContainsValue(i)) Console.Write("\u2502 "); else Console.Write(" "); } } void DrawMergePositionSpacer(int fromPos, int toPos) { for (int i = fromPos; i < toPos; i++) { if (commitPosition.ContainsValue(i)) Console.Write("\u2502\u2500"); else Console.Write("\u2500\u2500"); } } Console.ForegroundColor = graphColor; DrawPositionSpacer(0, commitPosition[c]); Console.ForegroundColor = defaultColor; if (taggedCommits.Contains(c)) Console.Write("v "); else Console.Write("* "); Console.ForegroundColor = graphColor; DrawPositionSpacer(commitPosition[c] + 1, maxPosition); Console.ForegroundColor = versionColor; Console.Write(versionCalculater.GetVersion(c).ToString(FieldCount)); Console.ForegroundColor = defaultColor; Console.Write(" - "); Console.Write(c.MessageShort.Trim()); if (++lineCount >= maxLines) { Console.WriteLine(); break; } if (c.Parents.Any()) { Commit p1 = c.Parents.First(); //Console.Write("Parent1: " + p1.Sha.Substring(0,8)); Console.WriteLine(); Console.ForegroundColor = graphColor; if (c.Parents.Count() > 1) { Commit p2 = c.Parents.Last(); int startPos; int endPos; if (commitPosition.ContainsKey(p1)) if (commitPosition[p1] != commitPosition[c]) { startPos = Math.Min(commitPosition[p1], commitPosition[c]); // something we already printed has the current commit as its parent, draw the line to that commit now DrawPositionSpacer(0, startPos); // Draw ├─┘ Console.Write("\u251C\u2500"); endPos = Math.Max(commitPosition[p1], commitPosition[c]); DrawMergePositionSpacer(startPos + 1, endPos); Console.Write("\u2518 "); DrawPositionSpacer(endPos + 1, maxPosition); Console.WriteLine(); commitPosition[c] = startPos; foreach (var kvp in commitPosition.Where((KeyValuePair kvp) => kvp.Value == endPos).ToList()) { commitPosition.Remove(kvp.Key); } } if (!commitPosition.ContainsKey(p2)) { DrawPositionSpacer(0, commitPosition[c]); // move out to an position out for the new branch int newPosition = commitPosition[c] + 1; while (commitPosition.ContainsValue(newPosition) && (newPosition <= commitPosition.Values.Max())) newPosition++; commitPosition[p2] = newPosition; commitPosition[p1] = commitPosition[c]; // Draw ├─┐ Console.Write("\u251C\u2500"); DrawMergePositionSpacer(commitPosition[c] + 1, commitPosition[p2]); Console.Write("\u2510 "); DrawPositionSpacer(commitPosition[p2] + 1, maxPosition); Console.WriteLine(); } else if (!commitPosition.ContainsKey(p1)) { commitPosition[p1] = commitPosition[c]; // this branch is merged several times startPos = Math.Min(commitPosition[p2], commitPosition[c]); DrawPositionSpacer(0, startPos); // draws something like: ├─┤ Console.Write("\u251C\u2500"); endPos = Math.Max(commitPosition[p2], commitPosition[c]); DrawMergePositionSpacer(startPos + 1, endPos); Console.Write("\u2524 "); DrawPositionSpacer(endPos + 1, maxPosition); Console.WriteLine(); } //else //{ // DrawPositionSpacer(0, commitPosition[p2]); // Console.Write("\u251C\u2500"); // DrawMergePositionSpacer(commitPosition[p2] + 1, commitPosition[c]); // Console.WriteLine("\u2524"); //} } else { if (!commitPosition.ContainsKey(p1)) commitPosition[p1] = commitPosition[c]; if (commitPosition[p1] != commitPosition[c]) { int startPos = Math.Min(commitPosition[p1], commitPosition[c]); DrawPositionSpacer(0, startPos); // Draw ├─┘ Console.Write("\u251C\u2500"); int endPos = Math.Max(commitPosition[p1], commitPosition[c]); DrawMergePositionSpacer(startPos + 1, endPos); Console.Write("\u2518 "); DrawPositionSpacer(endPos + 1, maxPosition); Console.WriteLine(); // c is now merged back, no need to keep track of it (or any other commit on this branch) // this way we can reuse the position for another branch foreach (var kvp in commitPosition.Where((KeyValuePair kvp) => kvp.Value == endPos).ToList()) { commitPosition.Remove(kvp.Key); } commitPosition[p1] = startPos; foreach (var kvp in commitPosition.Where((KeyValuePair kvp) => kvp.Value == startPos).ToList()) { if(kvp.Key != p1) commitPosition.Remove(kvp.Key); } } } } } } } return (int)ExitCodes.Success; } } /// /// Defines the UseVersion XML element that can be used as a child element to the File element in package.xml /// to indicate that a package should take its version from the AssemblyInfo in that file. /// [Display("UseVersion")] public class UseVersionData : ICustomPackageData { } }