using System; using System.Collections.Generic; using System.Diagnostics; using System.Text.RegularExpressions; namespace SourceGit.Git { /// /// Git commit information. /// public class Commit { private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----"; private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----"; /// /// SHA /// public string SHA { get; set; } /// /// Short SHA. /// public string ShortSHA => SHA.Substring(0, 8); /// /// Parent commit SHAs. /// public List Parents { get; set; } = new List(); /// /// Author /// public User Author { get; set; } = new User(); /// /// Committer. /// public User Committer { get; set; } = new User(); /// /// Subject /// public string Subject { get; set; } = ""; /// /// Extra message. /// public string Message { get; set; } = ""; /// /// HEAD commit? /// public bool IsHEAD { get; set; } = false; /// /// Merged in current branch? /// public bool IsMerged { get; set; } = false; /// /// X offset in graph /// public double GraphOffset { get; set; } = 0; /// /// Has decorators. /// public bool HasDecorators => Decorators.Count > 0; /// /// Decorators. /// public List Decorators { get; set; } = new List(); /// /// Read commits. /// /// Repository /// Limitations /// Parsed commits. public static List Load(Repository repo, string limit) { List commits = new List(); Commit current = null; bool bSkippingGpgsig = false; bool findHead = false; repo.RunCommand("log --date-order --decorate=full --pretty=raw " + limit, line => { if (bSkippingGpgsig) { if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) bSkippingGpgsig = false; return; } else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal)) { bSkippingGpgsig = true; return; } if (line.StartsWith("commit ", StringComparison.Ordinal)) { if (current != null) { current.Message = current.Message.TrimEnd(); commits.Add(current); } current = new Commit(); ParseSHA(current, line.Substring("commit ".Length)); if (!findHead) findHead = current.IsHEAD; return; } if (current == null) return; if (line.StartsWith("tree ", StringComparison.Ordinal)) { return; } else if (line.StartsWith("parent ", StringComparison.Ordinal)) { current.Parents.Add(line.Substring("parent ".Length)); } else if (line.StartsWith("author ", StringComparison.Ordinal)) { current.Author.Parse(line); } else if (line.StartsWith("committer ", StringComparison.Ordinal)) { current.Committer.Parse(line); } else if (string.IsNullOrEmpty(current.Subject)) { current.Subject = line.Trim(); } else { current.Message += (line.Trim() + "\n"); } }); if (current != null) { current.Message = current.Message.TrimEnd(); commits.Add(current); } if (!findHead && commits.Count > 0) { var startInfo = new ProcessStartInfo(); startInfo.FileName = Preference.Instance.GitExecutable; startInfo.Arguments = $"merge-base --is-ancestor {commits[0].SHA} HEAD"; startInfo.WorkingDirectory = repo.Path; startInfo.UseShellExecute = false; startInfo.CreateNoWindow = true; startInfo.RedirectStandardOutput = false; startInfo.RedirectStandardError = false; var proc = new Process() { StartInfo = startInfo }; proc.Start(); proc.WaitForExit(); commits[0].IsMerged = proc.ExitCode == 0; proc.Close(); } return commits; } /// /// Get changed file list. /// /// /// public List GetChanges(Repository repo) { var changes = new List(); var regex = new Regex(@"^[MADRC]\d*\s*.*$"); var errs = repo.RunCommand($"show --name-status {SHA}", line => { if (!regex.IsMatch(line)) return; var change = Change.Parse(line, true); if (change != null) changes.Add(change); }); if (errs != null) App.RaiseError(errs); return changes; } /// /// Get revision files. /// /// /// public List GetFiles(Repository repo) { var files = new List(); var errs = repo.RunCommand($"ls-tree --name-only -r {SHA}", line => { files.Add(line); }); if (errs != null) App.RaiseError(errs); return files; } /// /// Get file content. /// /// /// /// public string GetTextFileContent(Repository repo, string file) { var data = new List(); var isBinary = false; var count = 0; var errs = repo.RunCommand($"show {SHA}:\"{file}\"", line => { if (isBinary) return; count++; if (data.Count >= 1000) return; if (line.IndexOf('\0') >= 0) { isBinary = true; data.Clear(); data.Add("BINARY FILE PREVIEW NOT SUPPORTED!"); return; } data.Add(line); }); if (!isBinary && count > 1000) { data.Add("..."); data.Add($"Total {count} lines. Hide {count-1000} lines."); } if (errs != null) App.RaiseError(errs); return string.Join("\n", data); } private static void ParseSHA(Commit commit, string data) { var decoratorStart = data.IndexOf('('); if (decoratorStart < 0) { commit.SHA = data.Trim(); return; } commit.SHA = data.Substring(0, decoratorStart).Trim(); var subs = data.Substring(decoratorStart + 1).Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries); foreach (var sub in subs) { var d = sub.Trim(); if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) { commit.Decorators.Add(new Decorator() { Type = DecoratorType.Tag, Name = d.Substring(15).Trim() }); } else if (d.EndsWith("/HEAD")) { continue; } else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) { commit.IsHEAD = true; commit.Decorators.Add(new Decorator() { Type = DecoratorType.CurrentBranchHead, Name = d.Substring(19).Trim() }); } else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) { commit.Decorators.Add(new Decorator() { Type = DecoratorType.LocalBranchHead, Name = d.Substring(11).Trim() }); } else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) { commit.Decorators.Add(new Decorator() { Type = DecoratorType.RemoteBranchHead, Name = d.Substring(13).Trim() }); } } } } }