using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Windows.Threading; using System.Xml.Serialization; namespace SourceGit.Git { /// /// Git repository /// public class Repository { #region HOOKS public static Action OnOpen = null; public static Action OnClose = null; [XmlIgnore] public Action OnNavigateCommit = null; [XmlIgnore] public Action OnWorkingCopyChanged = null; [XmlIgnore] public Action OnTagChanged = null; [XmlIgnore] public Action OnStashChanged = null; [XmlIgnore] public Action OnBranchChanged = null; [XmlIgnore] public Action OnCommitsChanged = null; #endregion #region PROPERTIES_SAVED /// /// Storage path. /// public string Path { get; set; } /// /// Display name. /// public string Name { get; set; } /// /// Owner group. /// public string GroupId { get; set; } /// /// Last open time(File time format). /// public long LastOpenTime { get; set; } /// /// Filters for logs. /// public List LogFilters { get; set; } = new List(); /// /// Last 10 Commit message. /// public List CommitMsgRecords { get; set; } = new List(); #endregion #region PROPERTIES_RUNTIME private List cachedRemotes = new List(); private List cachedBranches = new List(); private List cachedTags = new List(); private FileSystemWatcher watcher = null; private DispatcherTimer timer = null; private bool isWatcherDisabled = false; private long nextUpdateTags = 0; private long nextUpdateLocalChanges = 0; private long nextUpdateStashes = 0; private long nextUpdateTree = 0; private string featurePrefix = null; private string releasePrefix = null; private string hotfixPrefix = null; #endregion #region METHOD_PROCESS /// /// Read git config /// /// /// public string GetConfig(string key) { var startInfo = new ProcessStartInfo(); startInfo.FileName = Preference.Instance.GitExecutable; startInfo.Arguments = $"config {key}"; startInfo.WorkingDirectory = Path; startInfo.UseShellExecute = false; startInfo.CreateNoWindow = true; startInfo.RedirectStandardOutput = true; startInfo.StandardOutputEncoding = Encoding.UTF8; var proc = new Process() { StartInfo = startInfo }; proc.Start(); var output = proc.StandardOutput.ReadToEnd(); proc.WaitForExit(); proc.Close(); return output.Trim(); } /// /// Configure git. /// /// /// public void SetConfig(string key, string value) { var startInfo = new ProcessStartInfo(); startInfo.FileName = Preference.Instance.GitExecutable; startInfo.Arguments = $"config {key} \"{value}\""; startInfo.WorkingDirectory = Path; startInfo.UseShellExecute = false; startInfo.CreateNoWindow = true; var proc = new Process() { StartInfo = startInfo }; proc.Start(); proc.WaitForExit(); proc.Close(); } /// /// Run git command without repository. /// /// Working directory. /// Arguments for running git command. /// Handler for output. /// Handle error as output. /// Errors if exists. public static string RunCommand(string cwd, string args, Action outputHandler, bool includeError = false) { var startInfo = new ProcessStartInfo(); startInfo.FileName = Preference.Instance.GitExecutable; startInfo.Arguments = "--no-pager -c core.quotepath=off " + args; startInfo.WorkingDirectory = cwd; startInfo.UseShellExecute = false; startInfo.CreateNoWindow = true; startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = true; startInfo.StandardOutputEncoding = Encoding.UTF8; startInfo.StandardErrorEncoding = Encoding.UTF8; var progressFilter = new Regex(@"\d+\%"); var errs = new List(); var proc = new Process() { StartInfo = startInfo }; proc.OutputDataReceived += (o, e) => { if (e.Data == null) return; outputHandler?.Invoke(e.Data); }; proc.ErrorDataReceived += (o, e) => { if (e.Data == null) return; if (includeError) outputHandler?.Invoke(e.Data); if (string.IsNullOrEmpty(e.Data)) return; if (progressFilter.IsMatch(e.Data)) return; if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal)) return; errs.Add(e.Data); }; proc.Start(); proc.BeginOutputReadLine(); proc.BeginErrorReadLine(); proc.WaitForExit(); int exitCode = proc.ExitCode; proc.Close(); if (exitCode != 0 && errs.Count > 0) { return string.Join("\n", errs); } else { return null; } } /// /// Create process for reading outputs/errors using git.exe /// /// Arguments for running git command. /// Handler for output. /// Handle error as output. /// Errors if exists. public string RunCommand(string args, Action outputHandler, bool includeError = false) { return RunCommand(Path, args, outputHandler, includeError); } /// /// Create process and redirect output to file. /// /// Git command arguments. /// File path to redirect output into. public void RunAndRedirect(string args, string redirectTo) { var startInfo = new ProcessStartInfo(); startInfo.FileName = Preference.Instance.GitExecutable; startInfo.Arguments = "--no-pager " + args; startInfo.WorkingDirectory = Path; startInfo.UseShellExecute = false; startInfo.CreateNoWindow = true; startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = true; var proc = new Process() { StartInfo = startInfo }; proc.Start(); using (var writer = new FileStream(redirectTo, FileMode.OpenOrCreate)) { proc.StandardOutput.BaseStream.CopyTo(writer); } proc.WaitForExit(); proc.Close(); } /// /// Assert command result and then update branches and commits. /// /// public void AssertCommand(string err) { if (!string.IsNullOrEmpty(err)) { App.RaiseError(err); } else { Branches(true); OnBranchChanged?.Invoke(); OnCommitsChanged?.Invoke(); OnWorkingCopyChanged?.Invoke(); } isWatcherDisabled = false; } #endregion #region METHOD_VALIDATIONS /// /// Is valid git directory. /// /// Local path. /// public static bool IsValid(string path) { var startInfo = new ProcessStartInfo(); startInfo.FileName = Preference.Instance.GitExecutable; startInfo.Arguments = "rev-parse --git-dir"; startInfo.WorkingDirectory = path; startInfo.UseShellExecute = false; startInfo.CreateNoWindow = true; try { var proc = new Process() { StartInfo = startInfo }; proc.Start(); proc.WaitForExit(); var test = proc.ExitCode == 0; proc.Close(); return test; } catch { return false; } } /// /// Is remote url valid. /// /// /// public static bool IsValidUrl(string url) { return !string.IsNullOrEmpty(url) && (url.StartsWith("http://", StringComparison.Ordinal) || url.StartsWith("https://", StringComparison.Ordinal) || url.StartsWith("git://", StringComparison.Ordinal) || url.StartsWith("ssh://", StringComparison.Ordinal) || url.StartsWith("file://", StringComparison.Ordinal)); } #endregion #region METHOD_OPEN_CLOSE /// /// Open repository. /// public void Open() { LastOpenTime = DateTime.Now.ToFileTime(); isWatcherDisabled = false; watcher = new FileSystemWatcher(); watcher.Path = Path; watcher.Filter = "*"; watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName; watcher.IncludeSubdirectories = true; watcher.Created += OnFSChanged; watcher.Renamed += OnFSChanged; watcher.Changed += OnFSChanged; watcher.Deleted += OnFSChanged; watcher.EnableRaisingEvents = true; timer = new DispatcherTimer(); timer.Tick += Tick; timer.Interval = TimeSpan.FromSeconds(.1); timer.Start(); featurePrefix = GetConfig("gitflow.prefix.feature"); releasePrefix = GetConfig("gitflow.prefix.release"); hotfixPrefix = GetConfig("gitflow.prefix.hotfix"); OnOpen?.Invoke(this); } /// /// Close repository. /// public void Close() { OnBranchChanged = null; OnCommitsChanged = null; OnTagChanged = null; OnStashChanged = null; OnWorkingCopyChanged = null; OnNavigateCommit = null; cachedBranches.Clear(); cachedRemotes.Clear(); cachedTags.Clear(); watcher.EnableRaisingEvents = false; watcher.Dispose(); timer.Stop(); watcher = null; timer = null; featurePrefix = null; releasePrefix = null; hotfixPrefix = null; OnClose?.Invoke(); } #endregion #region METHOD_WATCHER public void SetWatcherEnabled(bool enabled) { isWatcherDisabled = !enabled; } private void Tick(object sender, EventArgs e) { if (isWatcherDisabled) { nextUpdateLocalChanges = 0; nextUpdateStashes = 0; nextUpdateTags = 0; nextUpdateTree = 0; return; } var now = DateTime.Now.ToFileTime(); if (nextUpdateLocalChanges > 0 && now >= nextUpdateLocalChanges) { nextUpdateLocalChanges = 0; OnWorkingCopyChanged?.Invoke(); } if (nextUpdateTags > 0 && now >= nextUpdateTags) { nextUpdateTags = 0; OnTagChanged?.Invoke(); } if (nextUpdateStashes > 0 && now >= nextUpdateStashes) { nextUpdateStashes = 0; OnStashChanged?.Invoke(); } if (nextUpdateTree > 0 && now >= nextUpdateTree) { nextUpdateTree = 0; Branches(true); OnBranchChanged?.Invoke(); OnCommitsChanged?.Invoke(); } } private void OnFSChanged(object sender, FileSystemEventArgs e) { if (e.Name.StartsWith(".git", StringComparison.Ordinal)) { if (e.Name.Equals(".git") || e.Name.StartsWith(".git\\index")) return; if (e.Name.Equals(".gitignore") || e.Name.Equals(".gitattributes")) { nextUpdateLocalChanges = DateTime.Now.AddSeconds(1.5).ToFileTime(); } else if (e.Name.StartsWith(".git\\refs\\tags", StringComparison.Ordinal)) { nextUpdateTags = DateTime.Now.AddSeconds(.5).ToFileTime(); } else if (e.Name.StartsWith(".git\\refs\\stash", StringComparison.Ordinal)) { nextUpdateStashes = DateTime.Now.AddSeconds(.5).ToFileTime(); } else if (e.Name.EndsWith("_HEAD", StringComparison.Ordinal) || e.Name.StartsWith(".git\\refs\\heads", StringComparison.Ordinal) || e.Name.StartsWith(".git\\refs\\remotes", StringComparison.Ordinal)) { nextUpdateTree = DateTime.Now.AddSeconds(.5).ToFileTime(); } } else { nextUpdateLocalChanges = DateTime.Now.AddSeconds(1.5).ToFileTime(); } } #endregion #region METHOD_GITCOMMANDS /// /// Clone repository. /// /// Remote repository URL /// Folder to clone into /// Local name /// /// public static Repository Clone(string url, string folder, string name, Action onProgress) { var errs = RunCommand(folder, $"-c credential.helper=manager clone --progress --verbose --recurse-submodules {url} {name}", line => { if (line != null) onProgress?.Invoke(line); }, true); if (errs != null) { App.RaiseError(errs); return null; } var path = new DirectoryInfo(folder + "/" + name).FullName; var repo = Preference.Instance.AddRepository(path, ""); return repo; } /// /// Fetch remote changes /// /// /// /// public void Fetch(Remote remote, bool prune, Action onProgress) { isWatcherDisabled = true; var args = "-c credential.helper=manager fetch --progress --verbose "; if (prune) args += "--prune "; if (remote == null) { args += "--all"; } else { args += remote.Name; } var errs = RunCommand(args, line => { if (line != null) onProgress?.Invoke(line); }, true); AssertCommand(errs); } /// /// Pull remote changes. /// /// remote /// branch /// Progress message handler. /// Use rebase instead of merge. /// Auto stash local changes. /// Progress message handler. public void Pull(string remote, string branch, Action onProgress, bool rebase = false, bool autostash = false) { isWatcherDisabled = true; var args = "-c credential.helper=manager pull --verbose --progress "; var needPopStash = false; if (rebase) args += "--rebase "; if (autostash) { if (rebase) { args += "--autostash "; } else { var changes = LocalChanges(); if (changes.Count > 0) { var fatal = RunCommand("stash push -u -m \"PULL_AUTO_STASH\"", null); if (fatal != null) { App.RaiseError(fatal); isWatcherDisabled = false; return; } needPopStash = true; } } } var errs = RunCommand(args + remote + " " + branch, line => { if (line != null) onProgress?.Invoke(line); }, true); AssertCommand(errs); if (needPopStash) RunCommand("stash pop -q stash@{0}", null); } /// /// Push local branch to remote. /// /// Remote /// Local branch name /// Remote branch name /// Progress message handler. /// Push tags /// Create track reference /// Force push public void Push(string remote, string localBranch, string remoteBranch, Action onProgress, bool withTags = false, bool track = false, bool force = false) { isWatcherDisabled = true; var args = "-c credential.helper=manager push --progress --verbose "; if (withTags) args += "--tags "; if (track) args += "-u "; if (force) args += "--force-with-lease "; var errs = RunCommand(args + remote + " " + localBranch + ":" + remoteBranch, line => { if (line != null) onProgress?.Invoke(line); }, true); AssertCommand(errs); } /// /// Apply patch. /// /// /// public void Apply(string patch, string whitespaceMode) { isWatcherDisabled = true; var errs = RunCommand($"apply --whitespace={whitespaceMode} \"{patch}\"", null); if (errs != null) { App.RaiseError(errs); } else { OnWorkingCopyChanged?.Invoke(); } isWatcherDisabled = false; } /// /// Revert given commit. /// /// /// public void Revert(string commit, bool autoCommit) { isWatcherDisabled = true; var errs = RunCommand($"revert {commit} --no-edit" + (autoCommit ? "" : " --no-commit"), null); AssertCommand(errs); } /// /// Checkout /// /// Options. public void Checkout(string option) { isWatcherDisabled = true; var errs = RunCommand($"checkout {option}", null); AssertCommand(errs); } /// /// Merge given branch into current. /// /// /// public void Merge(string branch, string option) { isWatcherDisabled = true; var errs = RunCommand($"merge {branch} {option}", null); AssertCommand(errs); } /// /// Rebase current branch to revision /// /// /// public void Rebase(string revision, bool autoStash) { isWatcherDisabled = true; var args = $"rebase "; if (autoStash) args += "--autostash "; args += revision; var errs = RunCommand(args, null); AssertCommand(errs); } /// /// Reset. /// /// /// public void Reset(string revision, string mode = "") { isWatcherDisabled = true; var errs = RunCommand($"reset {mode} {revision}", null); AssertCommand(errs); } /// /// Cherry pick commit. /// /// /// public void CherryPick(string commit, bool noCommit) { isWatcherDisabled = true; var args = "cherry-pick "; args += noCommit ? "-n " : "--ff "; args += commit; var errs = RunCommand(args, null); AssertCommand(errs); } /// /// Stage(add) files to index. /// /// public void Stage(params string[] files) { isWatcherDisabled = true; var args = "add"; if (files == null || files.Length == 0) { args += " ."; } else { args += " --"; foreach (var file in files) args += $" \"{file}\""; } var errs = RunCommand(args, null); if (errs != null) App.RaiseError(errs); OnWorkingCopyChanged?.Invoke(); isWatcherDisabled = false; } /// /// Unstage files from index /// /// public void Unstage(params string[] files) { isWatcherDisabled = true; var args = "reset"; if (files != null && files.Length > 0) { args += " --"; foreach (var file in files) args += $" \"{file}\""; } var errs = RunCommand(args, null); if (errs != null) App.RaiseError(errs); OnWorkingCopyChanged?.Invoke(); isWatcherDisabled = false; } /// /// Discard changes. /// /// public void Discard(List changes) { isWatcherDisabled = true; if (changes == null || changes.Count == 0) { var errs = RunCommand("reset --hard HEAD", null); if (errs != null) { App.RaiseError(errs); isWatcherDisabled = false; return; } RunCommand("clean -qfd", null); } else { foreach (var change in changes) { if (change.WorkTree == Change.Status.Untracked || change.WorkTree == Change.Status.Added) { RunCommand($"clean -qfd -- \"{change.Path}\"", null); } else { RunCommand($"restore -- \"{change.Path}\"", null); } } } OnWorkingCopyChanged?.Invoke(); isWatcherDisabled = false; } /// /// Commit /// /// /// public bool DoCommit(string message, bool amend) { isWatcherDisabled = true; var file = System.IO.Path.Combine(Path, ".git", "COMMITMESSAGE"); File.WriteAllText(file, message); var args = $"commit --file=\"{file}\""; if (amend) args += " --amend --no-edit"; var errs = RunCommand(args, null); AssertCommand(errs); var branch = CurrentBranch(); OnNavigateCommit?.Invoke(branch.Head); return string.IsNullOrEmpty(errs); } /// /// Get all remotes of this repository. /// /// Force reload /// Remote collection public List Remotes(bool bForceReload = false) { if (cachedRemotes.Count == 0 || bForceReload) { cachedRemotes = Remote.Load(this); } return cachedRemotes; } /// /// Local changes in working copy. /// /// Changes. public List LocalChanges() { List changes = new List(); RunCommand("status -uall --porcelain", line => { if (!string.IsNullOrEmpty(line)) { var change = Change.Parse(line); if (change != null) changes.Add(change); } }); return changes; } /// /// Get total commit count. /// /// Number of total commits. public int TotalCommits() { int count = 0; RunCommand("rev-list --all --count", line => { if (!string.IsNullOrEmpty(line)) count = int.Parse(line.Trim()); }); return count; } /// /// Load commits. /// /// Extra limit arguments for `git log` /// Commit collection public List Commits(string limit = null) { return Commit.Load(this, (limit == null ? "" : limit)); ; } /// /// Load all branches. /// /// Force reload. /// Branches collection. public List Branches(bool bForceReload = false) { if (cachedBranches.Count == 0 || bForceReload) { cachedBranches = Branch.Load(this); } if (IsGitFlowEnabled()) { foreach (var b in cachedBranches) { if (b.IsLocal) { if (b.Name.StartsWith(featurePrefix)) { b.Kind = Branch.Type.Feature; } else if (b.Name.StartsWith(releasePrefix)) { b.Kind = Branch.Type.Release; } else if (b.Name.StartsWith(hotfixPrefix)) { b.Kind = Branch.Type.Hotfix; } } } } return cachedBranches; } /// /// Get current branch /// /// public Branch CurrentBranch() { foreach (var b in cachedBranches) { if (b.IsCurrent) return b; } return null; } /// /// Load all tags. /// /// /// public List Tags(bool bForceReload = false) { if (cachedTags.Count == 0 || bForceReload) { cachedTags = Tag.Load(this); } return cachedTags; } /// /// Get all stashes /// /// public List Stashes() { var reflog = new Regex(@"^Reflog: refs/(stash@\{\d+\}).*$"); var stashes = new List(); var current = null as Stash; var errs = RunCommand("stash list --pretty=raw", line => { if (line.StartsWith("commit ")) { if (current != null && !string.IsNullOrEmpty(current.Name)) stashes.Add(current); current = new Stash() { SHA = line.Substring(7, 8) }; return; } if (current == null) return; if (line.StartsWith("Reflog: refs/stash@")) { var match = reflog.Match(line); if (match.Success) current.Name = match.Groups[1].Value; } else if (line.StartsWith("Reflog message: ")) { current.Message = line.Substring(16); } else if (line.StartsWith("author ")) { current.Author.Parse(line); } }); if (current != null) stashes.Add(current); if (errs != null) App.RaiseError(errs); return stashes; } /// /// Diff /// /// /// /// /// /// public List Diff(string startRevision, string endRevision, string file, string orgFile = null) { var args = $"diff --ignore-cr-at-eol {startRevision} {endRevision} -- "; if (!string.IsNullOrEmpty(orgFile)) args += $"\"{orgFile}\" "; args += $"\"{file}\""; var data = new List(); var errs = RunCommand(args, line => data.Add(line)); if (errs != null) App.RaiseError(errs); return data; } /// /// Blame file. /// /// /// /// public Blame BlameFile(string file, string revision) { var regex = new Regex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)"); var blame = new Blame(); var current = null as Blame.Block; var errs = RunCommand($"blame -t {revision} -- \"{file}\"", line => { if (blame.IsBinary) return; if (string.IsNullOrEmpty(line)) return; if (line.IndexOf('\0') >= 0) { blame.IsBinary = true; blame.Blocks.Clear(); return; } var match = regex.Match(line); if (!match.Success) return; var commit = match.Groups[1].Value; var data = match.Groups[4].Value; if (current != null && current.CommitSHA == commit) { current.Content = current.Content + "\n" + data; } else { var timestamp = int.Parse(match.Groups[3].Value); var when = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timestamp).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); current = new Blame.Block() { CommitSHA = commit, Author = match.Groups[2].Value, Time = when, Content = data, }; if (current.Author == null) current.Author = ""; blame.Blocks.Add(current); } blame.LineCount++; }); if (errs != null) App.RaiseError(errs); return blame; } #endregion #region METHOD_GITFLOW /// /// Check if git-flow feature enabled /// /// public bool IsGitFlowEnabled() { return !string.IsNullOrEmpty(featurePrefix) && !string.IsNullOrEmpty(releasePrefix) && !string.IsNullOrEmpty(hotfixPrefix); } /// /// Get git-flow branch prefix. /// /// public string GetFeaturePrefix() { return featurePrefix; } public string GetReleasePrefix() { return releasePrefix; } public string GetHotfixPrefix() { return hotfixPrefix; } /// /// Enable git-flow /// /// /// /// /// /// /// public void EnableGitFlow(string master, string develop, string feature, string release, string hotfix, string version = "") { isWatcherDisabled = true; var branches = Branches(); var masterBranch = branches.Find(b => b.Name == master); var devBranch = branches.Find(b => b.Name == develop); var refreshBranches = false; if (masterBranch == null) { var errs = RunCommand($"branch --no-track {master}", null); if (errs != null) { App.RaiseError(errs); isWatcherDisabled = false; return; } refreshBranches = true; } if (devBranch == null) { var errs = RunCommand($"branch --no-track {develop}", null); if (errs != null) { App.RaiseError(errs); if (refreshBranches) { Branches(true); OnBranchChanged?.Invoke(); OnCommitsChanged?.Invoke(); OnWorkingCopyChanged?.Invoke(); } isWatcherDisabled = false; return; } refreshBranches = true; } SetConfig("gitflow.branch.master", master); SetConfig("gitflow.branch.develop", develop); SetConfig("gitflow.prefix.feature", feature); SetConfig("gitflow.prefix.bugfix", "bugfix"); SetConfig("gitflow.prefix.release", release); SetConfig("gitflow.prefix.hotfix", hotfix); SetConfig("gitflow.prefix.support", "support"); SetConfig("gitflow.prefix.versiontag", version); RunCommand("flow init -d", null); featurePrefix = GetConfig("gitflow.prefix.feature"); releasePrefix = GetConfig("gitflow.prefix.release"); hotfixPrefix = GetConfig("gitflow.prefix.hotfix"); if (!IsGitFlowEnabled()) App.RaiseError("Initialize Git-flow failed!"); if (refreshBranches) { Branches(true); OnBranchChanged?.Invoke(); OnCommitsChanged?.Invoke(); OnWorkingCopyChanged?.Invoke(); } isWatcherDisabled = false; } /// /// Start git-flow branch /// /// /// public void StartGitFlowBranch(Branch.Type type, string name) { isWatcherDisabled = true; string args; switch (type) { case Branch.Type.Feature: args = $"flow feature start {name}"; break; case Branch.Type.Release: args = $"flow release start {name}"; break; case Branch.Type.Hotfix: args = $"flow hotfix start {name}"; break; default: App.RaiseError("Bad git-flow branch type!"); return; } var errs = RunCommand(args, null); AssertCommand(errs); } /// /// Finish git-flow branch /// /// public void FinishGitFlowBranch(Branch branch) { isWatcherDisabled = true; string args; switch (branch.Kind) { case Branch.Type.Feature: args = $"flow feature finish {branch.Name.Substring(featurePrefix.Length)}"; break; case Branch.Type.Release: var releaseName = branch.Name.Substring(releasePrefix.Length); args = $"flow release finish {releaseName} -m \"Release done\""; break; case Branch.Type.Hotfix: var hotfixName = branch.Name.Substring(hotfixPrefix.Length); args = $"flow hotfix finish {hotfixName} -m \"Hotfix done\""; break; default: App.RaiseError("Bad git-flow branch type!"); return; } var errs = RunCommand(args, null); AssertCommand(errs); OnTagChanged?.Invoke(); } #endregion #region METHOD_COMMITMSG public void RecordCommitMessage(string message) { if (string.IsNullOrEmpty(message)) return; int exists = CommitMsgRecords.Count; if (exists > 0) { var last = CommitMsgRecords[0]; if (last == message) return; } if (exists >= 10) { CommitMsgRecords.RemoveRange(9, exists - 9); } CommitMsgRecords.Insert(0, message); } #endregion } }