From b26838ff68c467f1960d2ba3341c1d1e0f7e6170 Mon Sep 17 00:00:00 2001 From: leo Date: Sat, 11 Jan 2025 17:29:38 +0800 Subject: [PATCH] refactor: git version related commands * use `--pathspec-from-file=` in `git add` command if git >= 2.25.0 * use `--pathspec-from-file=` in `git stash push` command if git >= 2.26.0 * use `--staged` in `git stash push` command only if git >= 2.35.0 --- src/Commands/Add.cs | 7 ++ src/Commands/Stash.cs | 61 +++++++------- src/Commands/Version.cs | 19 ----- src/Converters/StringConverters.cs | 24 +----- src/Models/GitVersions.cs | 25 ++++++ src/Native/OS.cs | 107 +++++++++++++++++++++++-- src/ViewModels/StashChanges.cs | 123 ++++++++++++++++++++++++----- src/ViewModels/WorkingCopy.cs | 11 +++ src/Views/Preference.axaml | 5 +- src/Views/Preference.axaml.cs | 27 +++++-- 10 files changed, 306 insertions(+), 103 deletions(-) delete mode 100644 src/Commands/Version.cs create mode 100644 src/Models/GitVersions.cs diff --git a/src/Commands/Add.cs b/src/Commands/Add.cs index c3d1d3e6..e1b55b68 100644 --- a/src/Commands/Add.cs +++ b/src/Commands/Add.cs @@ -27,5 +27,12 @@ namespace SourceGit.Commands } Args = builder.ToString(); } + + public Add(string repo, string pathspecFromFile) + { + WorkingDirectory = repo; + Context = repo; + Args = $"add --pathspec-from-file=\"{pathspecFromFile}\""; + } } } diff --git a/src/Commands/Stash.cs b/src/Commands/Stash.cs index 77f1af53..40c917dd 100644 --- a/src/Commands/Stash.cs +++ b/src/Commands/Stash.cs @@ -17,47 +17,48 @@ namespace SourceGit.Commands return Exec(); } - public bool Push(List changes, string message, bool onlyStaged, bool keepIndex) + public bool Push(string message, List changes, bool keepIndex) { var builder = new StringBuilder(); builder.Append("stash push "); - if (onlyStaged) - builder.Append("--staged "); if (keepIndex) builder.Append("--keep-index "); builder.Append("-m \""); builder.Append(message); builder.Append("\" -- "); - if (onlyStaged) - { - foreach (var c in changes) - builder.Append($"\"{c.Path}\" "); - } - else - { - var needAdd = new List(); - foreach (var c in changes) - { - builder.Append($"\"{c.Path}\" "); + foreach (var c in changes) + builder.Append($"\"{c.Path}\" "); - if (c.WorkTree == Models.ChangeState.Added || c.WorkTree == Models.ChangeState.Untracked) - { - needAdd.Add(c); - if (needAdd.Count > 10) - { - new Add(WorkingDirectory, needAdd).Exec(); - needAdd.Clear(); - } - } - } - if (needAdd.Count > 0) - { - new Add(WorkingDirectory, needAdd).Exec(); - needAdd.Clear(); - } - } + Args = builder.ToString(); + return Exec(); + } + public bool Push(string message, string pathspecFromFile, bool keepIndex) + { + var builder = new StringBuilder(); + builder.Append("stash push --pathspec-from-file=\""); + builder.Append(pathspecFromFile); + builder.Append("\" "); + if (keepIndex) + builder.Append("--keep-index "); + builder.Append("-m \""); + builder.Append(message); + builder.Append("\""); + + Args = builder.ToString(); + return Exec(); + } + + public bool PushOnlyStaged(string message, bool keepIndex) + { + var builder = new StringBuilder(); + builder.Append("stash push --staged "); + if (keepIndex) + builder.Append("--keep-index "); + builder.Append("-m \""); + builder.Append(message); + builder.Append("\""); Args = builder.ToString(); return Exec(); } diff --git a/src/Commands/Version.cs b/src/Commands/Version.cs deleted file mode 100644 index ed7c6892..00000000 --- a/src/Commands/Version.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace SourceGit.Commands -{ - public class Version : Command - { - public Version() - { - Args = "--version"; - RaiseError = false; - } - - public string Query() - { - var rs = ReadToEnd(); - if (!rs.IsSuccess || string.IsNullOrWhiteSpace(rs.StdOut)) - return string.Empty; - return rs.StdOut.Trim().Substring("git version ".Length); - } - } -} diff --git a/src/Converters/StringConverters.cs b/src/Converters/StringConverters.cs index 585e0f02..e6f4237c 100644 --- a/src/Converters/StringConverters.cs +++ b/src/Converters/StringConverters.cs @@ -1,13 +1,12 @@ using System; using System.Globalization; -using System.Text.RegularExpressions; using Avalonia.Data.Converters; using Avalonia.Styling; namespace SourceGit.Converters { - public static partial class StringConverters + public static class StringConverters { public class ToLocaleConverter : IValueConverter { @@ -68,22 +67,6 @@ namespace SourceGit.Converters public static readonly FuncValueConverter ToShortSHA = new FuncValueConverter(v => v == null ? string.Empty : (v.Length > 10 ? v.Substring(0, 10) : v)); - public static readonly FuncValueConverter UnderRecommendGitVersion = - new(v => - { - var match = REG_GIT_VERSION().Match(v ?? ""); - if (match.Success) - { - var major = int.Parse(match.Groups[1].Value); - var minor = int.Parse(match.Groups[2].Value); - var build = int.Parse(match.Groups[3].Value); - - return new Version(major, minor, build) < MINIMAL_GIT_VERSION; - } - - return true; - }); - public static readonly FuncValueConverter TrimRefsPrefix = new FuncValueConverter(v => { @@ -95,10 +78,5 @@ namespace SourceGit.Converters return v.Substring(13); return v; }); - - [GeneratedRegex(@"^[\s\w]*(\d+)\.(\d+)[\.\-](\d+).*$")] - private static partial Regex REG_GIT_VERSION(); - - private static readonly Version MINIMAL_GIT_VERSION = new Version(2, 23, 0); } } diff --git a/src/Models/GitVersions.cs b/src/Models/GitVersions.cs new file mode 100644 index 00000000..394a9518 --- /dev/null +++ b/src/Models/GitVersions.cs @@ -0,0 +1,25 @@ +namespace SourceGit.Models +{ + public static class GitVersions + { + /// + /// The minimal version of Git that required by this app. + /// + public static readonly System.Version MINIMAL = new System.Version(2, 23, 0); + + /// + /// The minimal version of Git that supports the `add` command with the `--pathspec-from-file` option. + /// + public static readonly System.Version ADD_WITH_PATHSPECFILE = new System.Version(2, 25, 0); + + /// + /// The minimal version of Git that supports the `stash` command with the `--pathspec-from-file` option. + /// + public static readonly System.Version STASH_WITH_PATHSPECFILE = new System.Version(2, 26, 0); + + /// + /// The minimal version of Git that supports the `stash` command with the `--staged` option. + /// + public static readonly System.Version STASH_ONLY_STAGED = new System.Version(2, 35, 0); + } +} diff --git a/src/Native/OS.cs b/src/Native/OS.cs index c8c9e92d..177bbf9f 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -2,12 +2,14 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Text; +using System.Text.RegularExpressions; using Avalonia; namespace SourceGit.Native { - public static class OS + public static partial class OS { public interface IBackend { @@ -23,11 +25,51 @@ namespace SourceGit.Native void OpenWithDefaultEditor(string file); } - public static string DataDir { get; private set; } = string.Empty; - public static string GitExecutable { get; set; } = string.Empty; - public static string ShellOrTerminal { get; set; } = string.Empty; - public static List ExternalTools { get; set; } = []; - public static string CustomPathEnv { get; set; } = string.Empty; + public static string DataDir { + get; + private set; + } = string.Empty; + + public static string CustomPathEnv + { + get; + set; + } = string.Empty; + + public static string GitExecutable + { + get => _gitExecutable; + set + { + if (_gitExecutable != value) + { + _gitExecutable = value; + UpdateGitVersion(); + } + } + } + + public static string GitVersionString + { + get; + private set; + } = string.Empty; + + public static Version GitVersion + { + get; + private set; + } = new Version(0, 0, 0); + + public static string ShellOrTerminal { + get; + set; + } = string.Empty; + + public static List ExternalTools { + get; + set; + } = []; static OS() { @@ -123,6 +165,59 @@ namespace SourceGit.Native _backend.OpenWithDefaultEditor(file); } + private static void UpdateGitVersion() + { + if (string.IsNullOrEmpty(_gitExecutable) || !File.Exists(_gitExecutable)) + { + GitVersionString = string.Empty; + GitVersion = new Version(0, 0, 0); + return; + } + + var start = new ProcessStartInfo(); + start.FileName = _gitExecutable; + start.Arguments = "--version"; + start.UseShellExecute = false; + start.CreateNoWindow = true; + start.RedirectStandardOutput = true; + start.RedirectStandardError = true; + start.StandardOutputEncoding = Encoding.UTF8; + start.StandardErrorEncoding = Encoding.UTF8; + + var proc = new Process() { StartInfo = start }; + try + { + proc.Start(); + + var rs = proc.StandardOutput.ReadToEnd(); + proc.WaitForExit(); + if (proc.ExitCode == 0 && !string.IsNullOrWhiteSpace(rs)) + { + GitVersionString = rs.Trim(); + + var match = REG_GIT_VERSION().Match(GitVersionString); + if (match.Success) + { + var major = int.Parse(match.Groups[1].Value); + var minor = int.Parse(match.Groups[2].Value); + var build = int.Parse(match.Groups[3].Value); + GitVersion = new Version(major, minor, build); + GitVersionString = GitVersionString.Substring(11).Trim(); + } + } + } + catch + { + // Ignore errors + } + + proc.Close(); + } + + [GeneratedRegex(@"^git version[\s\w]*(\d+)\.(\d+)[\.\-](\d+).*$")] + private static partial Regex REG_GIT_VERSION(); + private static IBackend _backend = null; + private static string _gitExecutable = string.Empty; } } diff --git a/src/ViewModels/StashChanges.cs b/src/ViewModels/StashChanges.cs index 1ad3a0da..48c9478e 100644 --- a/src/ViewModels/StashChanges.cs +++ b/src/ViewModels/StashChanges.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; namespace SourceGit.ViewModels @@ -45,37 +47,122 @@ namespace SourceGit.ViewModels public override Task Sure() { - var jobs = _changes; - if (!HasSelectedFiles && !IncludeUntracked) - { - jobs = new List(); - foreach (var job in _changes) - { - if (job.WorkTree != Models.ChangeState.Untracked && job.WorkTree != Models.ChangeState.Added) - { - jobs.Add(job); - } - } - } - - if (jobs.Count == 0) - return null; - _repo.SetWatcherEnabled(false); ProgressDescription = $"Stash changes ..."; return Task.Run(() => { - var succ = new Commands.Stash(_repo.FullPath).Push(jobs, Message, !HasSelectedFiles && OnlyStaged, KeepIndex); + var succ = false; + + if (!HasSelectedFiles) + { + if (OnlyStaged) + { + if (Native.OS.GitVersion >= Models.GitVersions.STASH_ONLY_STAGED) + { + succ = new Commands.Stash(_repo.FullPath).PushOnlyStaged(Message, KeepIndex); + } + else + { + var staged = new List(); + foreach (var c in _changes) + { + if (c.Index != Models.ChangeState.None && c.Index != Models.ChangeState.Untracked) + staged.Add(c); + } + + succ = StashWithChanges(staged); + } + } + else + { + if (IncludeUntracked) + AddUntracked(_changes); + succ = StashWithChanges(_changes); + } + } + else + { + AddUntracked(_changes); + succ = StashWithChanges(_changes); + } + CallUIThread(() => { _repo.MarkWorkingCopyDirtyManually(); _repo.SetWatcherEnabled(true); }); + return succ; }); } + private void AddUntracked(List changes) + { + var toBeAdded = new List(); + foreach (var c in changes) + { + if (c.WorkTree == Models.ChangeState.Added || c.WorkTree == Models.ChangeState.Untracked) + toBeAdded.Add(c); + } + + if (toBeAdded.Count == 0) + return; + + if (Native.OS.GitVersion >= Models.GitVersions.ADD_WITH_PATHSPECFILE) + { + var paths = new List(); + foreach (var c in toBeAdded) + paths.Add(c.Path); + + var tmpFile = Path.GetTempFileName(); + File.WriteAllLines(tmpFile, paths); + new Commands.Add(_repo.FullPath, tmpFile).Exec(); + File.Delete(tmpFile); + } + else + { + for (int i = 0; i < toBeAdded.Count; i += 10) + { + var count = Math.Min(10, toBeAdded.Count - i); + var step = toBeAdded.GetRange(i, count); + new Commands.Add(_repo.FullPath, step).Exec(); + } + } + } + + private bool StashWithChanges(List changes) + { + if (changes.Count == 0) + return true; + + var succ = false; + if (Native.OS.GitVersion >= Models.GitVersions.STASH_WITH_PATHSPECFILE) + { + var paths = new List(); + foreach (var c in changes) + paths.Add(c.Path); + + var tmpFile = Path.GetTempFileName(); + File.WriteAllLines(tmpFile, paths); + succ = new Commands.Stash(_repo.FullPath).Push(Message, tmpFile, KeepIndex); + File.Delete(tmpFile); + } + else + { + for (int i = 0; i < changes.Count; i += 10) + { + var count = Math.Min(10, changes.Count - i); + var step = changes.GetRange(i, count); + succ = new Commands.Stash(_repo.FullPath).Push(Message, step, KeepIndex); + if (!succ) + break; + } + } + + return succ; + } + private readonly Repository _repo = null; private readonly List _changes = null; } diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index bb809ea5..b0f608aa 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -347,6 +347,17 @@ namespace SourceGit.ViewModels { await Task.Run(() => new Commands.Add(_repo.FullPath, _repo.IncludeUntracked).Exec()); } + else if (Native.OS.GitVersion >= Models.GitVersions.ADD_WITH_PATHSPECFILE) + { + var paths = new List(); + foreach (var c in changes) + paths.Add(c.Path); + + var tmpFile = Path.GetTempFileName(); + File.WriteAllLines(tmpFile, paths); + await Task.Run(() => new Commands.Add(_repo.FullPath, tmpFile).Exec()); + File.Delete(tmpFile); + } else { for (int i = 0; i < changes.Count; i += 10) diff --git a/src/Views/Preference.axaml b/src/Views/Preference.axaml index c16becff..1c01578a 100644 --- a/src/Views/Preference.axaml +++ b/src/Views/Preference.axaml @@ -263,7 +263,8 @@ + Text="{Binding GitInstallPath, Mode=TwoWay}" + TextChanged="OnGitInstallPathChanged">