refactor: git version related commands
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions

* use `--pathspec-from-file=<FILE>` in `git add` command if git >= 2.25.0
* use `--pathspec-from-file=<FILE>` in `git stash push` command if git >= 2.26.0
* use `--staged` in `git stash push` command only if git >= 2.35.0
This commit is contained in:
leo 2025-01-11 17:29:38 +08:00
parent c939308e4c
commit b26838ff68
No known key found for this signature in database
10 changed files with 306 additions and 103 deletions

View file

@ -27,5 +27,12 @@ namespace SourceGit.Commands
} }
Args = builder.ToString(); Args = builder.ToString();
} }
public Add(string repo, string pathspecFromFile)
{
WorkingDirectory = repo;
Context = repo;
Args = $"add --pathspec-from-file=\"{pathspecFromFile}\"";
}
} }
} }

View file

@ -17,47 +17,48 @@ namespace SourceGit.Commands
return Exec(); return Exec();
} }
public bool Push(List<Models.Change> changes, string message, bool onlyStaged, bool keepIndex) public bool Push(string message, List<Models.Change> changes, bool keepIndex)
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.Append("stash push "); builder.Append("stash push ");
if (onlyStaged)
builder.Append("--staged ");
if (keepIndex) if (keepIndex)
builder.Append("--keep-index "); builder.Append("--keep-index ");
builder.Append("-m \""); builder.Append("-m \"");
builder.Append(message); builder.Append(message);
builder.Append("\" -- "); builder.Append("\" -- ");
if (onlyStaged)
{
foreach (var c in changes) foreach (var c in changes)
builder.Append($"\"{c.Path}\" "); builder.Append($"\"{c.Path}\" ");
}
else
{
var needAdd = new List<Models.Change>();
foreach (var c in changes)
{
builder.Append($"\"{c.Path}\" ");
if (c.WorkTree == Models.ChangeState.Added || c.WorkTree == Models.ChangeState.Untracked) Args = builder.ToString();
{ return Exec();
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();
}
} }
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(); Args = builder.ToString();
return Exec(); return Exec();
} }

View file

@ -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);
}
}
}

View file

@ -1,13 +1,12 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Text.RegularExpressions;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Styling; using Avalonia.Styling;
namespace SourceGit.Converters namespace SourceGit.Converters
{ {
public static partial class StringConverters public static class StringConverters
{ {
public class ToLocaleConverter : IValueConverter public class ToLocaleConverter : IValueConverter
{ {
@ -68,22 +67,6 @@ namespace SourceGit.Converters
public static readonly FuncValueConverter<string, string> ToShortSHA = public static readonly FuncValueConverter<string, string> ToShortSHA =
new FuncValueConverter<string, string>(v => v == null ? string.Empty : (v.Length > 10 ? v.Substring(0, 10) : v)); new FuncValueConverter<string, string>(v => v == null ? string.Empty : (v.Length > 10 ? v.Substring(0, 10) : v));
public static readonly FuncValueConverter<string, bool> 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<string, string> TrimRefsPrefix = public static readonly FuncValueConverter<string, string> TrimRefsPrefix =
new FuncValueConverter<string, string>(v => new FuncValueConverter<string, string>(v =>
{ {
@ -95,10 +78,5 @@ namespace SourceGit.Converters
return v.Substring(13); return v.Substring(13);
return v; 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);
} }
} }

25
src/Models/GitVersions.cs Normal file
View file

@ -0,0 +1,25 @@
namespace SourceGit.Models
{
public static class GitVersions
{
/// <summary>
/// The minimal version of Git that required by this app.
/// </summary>
public static readonly System.Version MINIMAL = new System.Version(2, 23, 0);
/// <summary>
/// The minimal version of Git that supports the `add` command with the `--pathspec-from-file` option.
/// </summary>
public static readonly System.Version ADD_WITH_PATHSPECFILE = new System.Version(2, 25, 0);
/// <summary>
/// The minimal version of Git that supports the `stash` command with the `--pathspec-from-file` option.
/// </summary>
public static readonly System.Version STASH_WITH_PATHSPECFILE = new System.Version(2, 26, 0);
/// <summary>
/// The minimal version of Git that supports the `stash` command with the `--staged` option.
/// </summary>
public static readonly System.Version STASH_ONLY_STAGED = new System.Version(2, 35, 0);
}
}

View file

@ -2,12 +2,14 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Avalonia; using Avalonia;
namespace SourceGit.Native namespace SourceGit.Native
{ {
public static class OS public static partial class OS
{ {
public interface IBackend public interface IBackend
{ {
@ -23,11 +25,51 @@ namespace SourceGit.Native
void OpenWithDefaultEditor(string file); void OpenWithDefaultEditor(string file);
} }
public static string DataDir { get; private set; } = string.Empty; public static string DataDir {
public static string GitExecutable { get; set; } = string.Empty; get;
public static string ShellOrTerminal { get; set; } = string.Empty; private set;
public static List<Models.ExternalTool> ExternalTools { get; set; } = []; } = string.Empty;
public static string CustomPathEnv { get; 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<Models.ExternalTool> ExternalTools {
get;
set;
} = [];
static OS() static OS()
{ {
@ -123,6 +165,59 @@ namespace SourceGit.Native
_backend.OpenWithDefaultEditor(file); _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 IBackend _backend = null;
private static string _gitExecutable = string.Empty;
} }
} }

View file

@ -1,4 +1,6 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SourceGit.ViewModels namespace SourceGit.ViewModels
@ -45,37 +47,122 @@ namespace SourceGit.ViewModels
public override Task<bool> Sure() public override Task<bool> Sure()
{ {
var jobs = _changes;
if (!HasSelectedFiles && !IncludeUntracked)
{
jobs = new List<Models.Change>();
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); _repo.SetWatcherEnabled(false);
ProgressDescription = $"Stash changes ..."; ProgressDescription = $"Stash changes ...";
return Task.Run(() => 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<Models.Change>();
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(() => CallUIThread(() =>
{ {
_repo.MarkWorkingCopyDirtyManually(); _repo.MarkWorkingCopyDirtyManually();
_repo.SetWatcherEnabled(true); _repo.SetWatcherEnabled(true);
}); });
return succ; return succ;
}); });
} }
private void AddUntracked(List<Models.Change> changes)
{
var toBeAdded = new List<Models.Change>();
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<string>();
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<Models.Change> changes)
{
if (changes.Count == 0)
return true;
var succ = false;
if (Native.OS.GitVersion >= Models.GitVersions.STASH_WITH_PATHSPECFILE)
{
var paths = new List<string>();
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 Repository _repo = null;
private readonly List<Models.Change> _changes = null; private readonly List<Models.Change> _changes = null;
} }

View file

@ -347,6 +347,17 @@ namespace SourceGit.ViewModels
{ {
await Task.Run(() => new Commands.Add(_repo.FullPath, _repo.IncludeUntracked).Exec()); 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<string>();
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 else
{ {
for (int i = 0; i < changes.Count; i += 10) for (int i = 0; i < changes.Count; i += 10)

View file

@ -263,7 +263,8 @@
<TextBox Grid.Row="0" Grid.Column="1" <TextBox Grid.Row="0" Grid.Column="1"
Height="28" Height="28"
CornerRadius="3" CornerRadius="3"
Text="{Binding GitInstallPath, Mode=TwoWay}"> Text="{Binding GitInstallPath, Mode=TwoWay}"
TextChanged="OnGitInstallPathChanged">
<TextBox.InnerRightContent> <TextBox.InnerRightContent>
<Button Classes="icon_button" Width="30" Height="30" Click="SelectGitExecutable"> <Button Classes="icon_button" Width="30" Height="30" Click="SelectGitExecutable">
<Path Data="{StaticResource Icons.Folder.Open}" Fill="{DynamicResource Brush.FG1}"/> <Path Data="{StaticResource Icons.Folder.Open}" Fill="{DynamicResource Brush.FG1}"/>
@ -282,7 +283,7 @@
<Border Background="Transparent" <Border Background="Transparent"
ToolTip.Tip="{DynamicResource Text.Preference.Git.Invalid}" ToolTip.Tip="{DynamicResource Text.Preference.Git.Invalid}"
IsVisible="{Binding #ThisControl.GitVersion, Converter={x:Static c:StringConverters.UnderRecommendGitVersion}}"> IsVisible="{Binding #ThisControl.ShowGitVersionWarning}">
<Path Width="14" Height="14" Data="{StaticResource Icons.Error}" Fill="Red"/> <Path Width="14" Height="14" Data="{StaticResource Icons.Error}" Fill="Red"/>
</Border> </Border>
</StackPanel> </StackPanel>

View file

@ -37,6 +37,15 @@ namespace SourceGit.Views
set => SetValue(GitVersionProperty, value); set => SetValue(GitVersionProperty, value);
} }
public static readonly StyledProperty<bool> ShowGitVersionWarningProperty =
AvaloniaProperty.Register<Preference, bool>(nameof(ShowGitVersionWarning));
public bool ShowGitVersionWarning
{
get => GetValue(ShowGitVersionWarningProperty);
set => SetValue(ShowGitVersionWarningProperty, value);
}
public bool EnableGPGCommitSigning public bool EnableGPGCommitSigning
{ {
get; get;
@ -93,7 +102,6 @@ namespace SourceGit.Views
var pref = ViewModels.Preference.Instance; var pref = ViewModels.Preference.Instance;
DataContext = pref; DataContext = pref;
var ver = string.Empty;
if (pref.IsGitConfigured()) if (pref.IsGitConfigured())
{ {
var config = new Commands.Config(null).ListAll(); var config = new Commands.Config(null).ListAll();
@ -122,12 +130,10 @@ namespace SourceGit.Views
EnableHTTPSSLVerify = sslVerify == "true"; EnableHTTPSSLVerify = sslVerify == "true";
else else
EnableHTTPSSLVerify = true; EnableHTTPSSLVerify = true;
ver = new Commands.Version().Query();
} }
UpdateGitVersion();
InitializeComponent(); InitializeComponent();
GitVersion = ver;
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@ -207,7 +213,7 @@ namespace SourceGit.Views
if (selected.Count == 1) if (selected.Count == 1)
{ {
ViewModels.Preference.Instance.GitInstallPath = selected[0].Path.LocalPath; ViewModels.Preference.Instance.GitInstallPath = selected[0].Path.LocalPath;
GitVersion = new Commands.Version().Query(); UpdateGitVersion();
} }
e.Handled = true; e.Handled = true;
@ -328,6 +334,11 @@ namespace SourceGit.Views
e.Handled = true; e.Handled = true;
} }
private void OnGitInstallPathChanged(object sender, TextChangedEventArgs e)
{
UpdateGitVersion();
}
private void OnAddOpenAIService(object sender, RoutedEventArgs e) private void OnAddOpenAIService(object sender, RoutedEventArgs e)
{ {
var service = new Models.OpenAIService() { Name = "Unnamed Service" }; var service = new Models.OpenAIService() { Name = "Unnamed Service" };
@ -346,5 +357,11 @@ namespace SourceGit.Views
SelectedOpenAIService = null; SelectedOpenAIService = null;
e.Handled = true; e.Handled = true;
} }
private void UpdateGitVersion()
{
GitVersion = Native.OS.GitVersionString;
ShowGitVersionWarning = !string.IsNullOrEmpty(GitVersion) && Native.OS.GitVersion < Models.GitVersions.MINIMAL;
}
} }
} }