This commit is contained in:
Bernat Borràs Civil 2025-04-11 16:50:28 +02:00
commit 75b15c455e
284 changed files with 10116 additions and 5764 deletions

View file

@ -12,7 +12,7 @@ namespace SourceGit.Commands
Args = includeUntracked ? "add ." : "add -u .";
}
public Add(string repo, List<Models.Change> changes)
public Add(string repo, List<string> changes)
{
WorkingDirectory = repo;
Context = repo;
@ -22,7 +22,7 @@ namespace SourceGit.Commands
foreach (var c in changes)
{
builder.Append(" \"");
builder.Append(c.Path);
builder.Append(c);
builder.Append("\"");
}
Args = builder.ToString();

View file

@ -54,21 +54,14 @@
public static bool DeleteRemote(string repo, string remote, string name)
{
bool exists = new Remote(repo).HasBranch(remote, name);
if (exists)
return new Push(repo, remote, $"refs/heads/{name}", true).Exec();
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
bool exists = new Remote(repo).HasBranch(remote, name);
if (exists)
{
cmd.SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
cmd.Args = $"push {remote} --delete {name}";
}
else
{
cmd.Args = $"branch -D -r {remote}/{name}";
}
cmd.Args = $"branch -D -r {remote}/{name}";
return cmd.Exec();
}
}

View file

@ -12,7 +12,7 @@ namespace SourceGit.Commands
WorkingDirectory = path;
TraitErrorAsOutput = true;
SSHKey = sshKey;
Args = "clone --progress --verbose --recurse-submodules ";
Args = "clone --progress --verbose ";
if (!string.IsNullOrEmpty(extraArgs))
Args += $"{extraArgs} ";

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Avalonia.Threading;
@ -10,11 +11,6 @@ namespace SourceGit.Commands
{
public partial class Command
{
public class CancelToken
{
public bool Requested { get; set; } = false;
}
public class ReadToEndResult
{
public bool IsSuccess { get; set; } = false;
@ -30,7 +26,7 @@ namespace SourceGit.Commands
}
public string Context { get; set; } = string.Empty;
public CancelToken Cancel { get; set; } = null;
public CancellationToken CancellationToken { get; set; } = CancellationToken.None;
public string WorkingDirectory { get; set; } = null;
public EditorType Editor { get; set; } = EditorType.CoreEditor; // Only used in Exec() mode
public string SSHKey { get; set; } = string.Empty;
@ -43,36 +39,15 @@ namespace SourceGit.Commands
var start = CreateGitStartInfo();
var errs = new List<string>();
var proc = new Process() { StartInfo = start };
var isCancelled = false;
proc.OutputDataReceived += (_, e) =>
{
if (Cancel != null && Cancel.Requested)
{
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
if (!proc.HasExited)
proc.Kill(true);
return;
}
if (e.Data != null)
OnReadline(e.Data);
};
proc.ErrorDataReceived += (_, e) =>
{
if (Cancel != null && Cancel.Requested)
{
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
if (!proc.HasExited)
proc.Kill(true);
return;
}
if (string.IsNullOrEmpty(e.Data))
{
errs.Add(string.Empty);
@ -97,9 +72,25 @@ namespace SourceGit.Commands
errs.Add(e.Data);
};
var dummy = null as Process;
var dummyProcLock = new object();
try
{
proc.Start();
// It not safe, please only use `CancellationToken` in readonly commands.
if (CancellationToken.CanBeCanceled)
{
dummy = proc;
CancellationToken.Register(() =>
{
lock (dummyProcLock)
{
if (dummy is { HasExited: false })
dummy.Kill();
}
});
}
}
catch (Exception e)
{
@ -113,10 +104,18 @@ namespace SourceGit.Commands
proc.BeginErrorReadLine();
proc.WaitForExit();
if (dummy != null)
{
lock (dummyProcLock)
{
dummy = null;
}
}
int exitCode = proc.ExitCode;
proc.Close();
if (!isCancelled && exitCode != 0)
if (!CancellationToken.IsCancellationRequested && exitCode != 0)
{
if (RaiseError)
{
@ -192,13 +191,12 @@ namespace SourceGit.Commands
if (!start.Environment.ContainsKey("GIT_SSH_COMMAND") && !string.IsNullOrEmpty(SSHKey))
start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}'");
// Force using en_US.UTF-8 locale to avoid GCM crash
// Force using en_US.UTF-8 locale
if (OperatingSystem.IsLinux())
start.Environment.Add("LANG", "en_US.UTF-8");
// Fix macOS `PATH` env
if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv))
start.Environment.Add("PATH", Native.OS.CustomPathEnv);
{
start.Environment.Add("LANG", "C");
start.Environment.Add("LC_ALL", "C");
}
// Force using this app as git editor.
switch (Editor)

View file

@ -6,8 +6,10 @@ namespace SourceGit.Commands
{
public partial class CompareRevisions : Command
{
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
[GeneratedRegex(@"^([MADC])\s+(.+)$")]
private static partial Regex REG_FORMAT();
[GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)$")]
private static partial Regex REG_RENAME_FORMAT();
public CompareRevisions(string repo, string start, string end)
{
@ -18,6 +20,15 @@ namespace SourceGit.Commands
Args = $"diff --name-status {based} {end}";
}
public CompareRevisions(string repo, string start, string end, string path)
{
WorkingDirectory = repo;
Context = repo;
var based = string.IsNullOrEmpty(start) ? "-R" : start;
Args = $"diff --name-status {based} {end} -- \"{path}\"";
}
public List<Models.Change> Result()
{
Exec();
@ -29,7 +40,17 @@ namespace SourceGit.Commands
{
var match = REG_FORMAT().Match(line);
if (!match.Success)
{
match = REG_RENAME_FORMAT().Match(line);
if (match.Success)
{
var renamed = new Models.Change() { Path = match.Groups[1].Value };
renamed.Set(Models.ChangeState.Renamed);
_changes.Add(renamed);
}
return;
}
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
@ -48,10 +69,6 @@ namespace SourceGit.Commands
change.Set(Models.ChangeState.Deleted);
_changes.Add(change);
break;
case 'R':
change.Set(Models.ChangeState.Renamed);
_changes.Add(change);
break;
case 'C':
change.Set(Models.ChangeState.Copied);
_changes.Add(change);

View file

@ -29,7 +29,7 @@ namespace SourceGit.Commands
var rs = new Dictionary<string, string>();
if (output.IsSuccess)
{
var lines = output.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var idx = line.IndexOf('=', StringComparison.Ordinal);

View file

@ -8,7 +8,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = "status -uno --ignore-submodules=dirty --porcelain";
Args = "--no-optional-locks status -uno --ignore-submodules=dirty --porcelain";
}
public int Result()
@ -16,7 +16,7 @@ namespace SourceGit.Commands
var rs = ReadToEnd();
if (rs.IsSuccess)
{
var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
return lines.Length;
}

View file

@ -68,6 +68,18 @@ namespace SourceGit.Commands
return;
}
if (line.StartsWith("deleted file mode ", StringComparison.Ordinal))
{
_result.OldMode = line.Substring(18);
return;
}
if (line.StartsWith("new file mode ", StringComparison.Ordinal))
{
_result.NewMode = line.Substring(14);
return;
}
if (_result.IsBinary)
return;

View file

@ -8,7 +8,26 @@ namespace SourceGit.Commands
{
public static class ExecuteCustomAction
{
public static void Run(string repo, string file, string args, Action<string> outputHandler)
public static void Run(string repo, string file, string args)
{
var start = new ProcessStartInfo();
start.FileName = file;
start.Arguments = args;
start.UseShellExecute = false;
start.CreateNoWindow = true;
start.WorkingDirectory = repo;
try
{
Process.Start(start);
}
catch (Exception e)
{
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, e.Message));
}
}
public static void RunAndWait(string repo, string file, string args, Action<string> outputHandler)
{
var start = new ProcessStartInfo();
start.FileName = file;
@ -21,14 +40,6 @@ namespace SourceGit.Commands
start.StandardErrorEncoding = Encoding.UTF8;
start.WorkingDirectory = repo;
// Force using en_US.UTF-8 locale to avoid GCM crash
if (OperatingSystem.IsLinux())
start.Environment.Add("LANG", "en_US.UTF-8");
// Fix macOS `PATH` env
if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv))
start.Environment.Add("PATH", Native.OS.CustomPathEnv);
var proc = new Process() { StartInfo = start };
var builder = new StringBuilder();
@ -57,19 +68,14 @@ namespace SourceGit.Commands
var exitCode = proc.ExitCode;
if (exitCode != 0)
{
var errMsg = builder.ToString();
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, errMsg);
});
var errMsg = builder.ToString().Trim();
if (!string.IsNullOrEmpty(errMsg))
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, errMsg));
}
}
catch (Exception e)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, e.Message);
});
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, e.Message));
}
proc.Close();

View file

@ -4,7 +4,7 @@ namespace SourceGit.Commands
{
public class Fetch : Command
{
public Fetch(string repo, string remote, bool noTags, bool prune, bool force, Action<string> outputHandler)
public Fetch(string repo, string remote, bool noTags, bool force, Action<string> outputHandler)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
@ -21,9 +21,6 @@ namespace SourceGit.Commands
if (force)
Args += "--force ";
if (prune)
Args += "--prune ";
Args += remote;
}

View file

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Text;
using System.Threading;
using Avalonia.Threading;
namespace SourceGit.Commands
{
/// <summary>
@ -20,82 +22,78 @@ namespace SourceGit.Commands
}
}
public GenerateCommitMessage(Models.OpenAIService service, string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onProgress)
public GenerateCommitMessage(Models.OpenAIService service, string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onResponse)
{
_service = service;
_repo = repo;
_changes = changes;
_cancelToken = cancelToken;
_onProgress = onProgress;
_onResponse = onResponse;
}
public string Result()
public void Exec()
{
try
{
var summarybuilder = new StringBuilder();
var bodyBuilder = new StringBuilder();
_onResponse?.Invoke("Waiting for pre-file analyzing to completed...\n\n");
var responseBuilder = new StringBuilder();
var summaryBuilder = new StringBuilder();
foreach (var change in _changes)
{
if (_cancelToken.IsCancellationRequested)
return "";
return;
_onProgress?.Invoke($"Analyzing {change.Path}...");
responseBuilder.Append("- ");
summaryBuilder.Append("- ");
var summary = GenerateChangeSummary(change);
summarybuilder.Append("- ");
summarybuilder.Append(summary);
summarybuilder.Append("(file: ");
summarybuilder.Append(change.Path);
summarybuilder.Append(")");
summarybuilder.AppendLine();
var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
if (rs.IsSuccess)
{
_service.Chat(
_service.AnalyzeDiffPrompt,
$"Here is the `git diff` output: {rs.StdOut}",
_cancelToken,
update =>
{
responseBuilder.Append(update);
summaryBuilder.Append(update);
bodyBuilder.Append("- ");
bodyBuilder.Append(summary);
bodyBuilder.AppendLine();
_onResponse?.Invoke($"Waiting for pre-file analyzing to completed...\n\n{responseBuilder}");
});
}
responseBuilder.Append("\n");
summaryBuilder.Append("(file: ");
summaryBuilder.Append(change.Path);
summaryBuilder.Append(")\n");
}
if (_cancelToken.IsCancellationRequested)
return "";
return;
_onProgress?.Invoke($"Generating commit message...");
var body = bodyBuilder.ToString();
var subject = GenerateSubject(summarybuilder.ToString());
return string.Format("{0}\n\n{1}", subject, body);
var responseBody = responseBuilder.ToString();
var subjectBuilder = new StringBuilder();
_service.Chat(
_service.GenerateSubjectPrompt,
$"Here are the summaries changes:\n{summaryBuilder}",
_cancelToken,
update =>
{
subjectBuilder.Append(update);
_onResponse?.Invoke($"{subjectBuilder}\n\n{responseBody}");
});
}
catch (Exception e)
{
App.RaiseException(_repo, $"Failed to generate commit message: {e}");
return "";
Dispatcher.UIThread.Post(() => App.RaiseException(_repo, $"Failed to generate commit message: {e}"));
}
}
private string GenerateChangeSummary(Models.Change change)
{
var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
var diff = rs.IsSuccess ? rs.StdOut : "unknown change";
var rsp = _service.Chat(_service.AnalyzeDiffPrompt, $"Here is the `git diff` output: {diff}", _cancelToken);
if (rsp != null && rsp.Choices.Count > 0)
return rsp.Choices[0].Message.Content;
return string.Empty;
}
private string GenerateSubject(string summary)
{
var rsp = _service.Chat(_service.GenerateSubjectPrompt, $"Here are the summaries changes:\n{summary}", _cancelToken);
if (rsp != null && rsp.Choices.Count > 0)
return rsp.Choices[0].Message.Content;
return string.Empty;
}
private Models.OpenAIService _service;
private string _repo;
private List<Models.Change> _changes;
private CancellationToken _cancelToken;
private Action<string> _onProgress;
private Action<string> _onResponse;
}
}

View file

@ -0,0 +1,24 @@
using System.IO;
namespace SourceGit.Commands
{
public class IsBareRepository : Command
{
public IsBareRepository(string path)
{
WorkingDirectory = path;
Args = "rev-parse --is-bare-repository";
}
public bool Result()
{
if (!Directory.Exists(Path.Combine(WorkingDirectory, "refs")) ||
!Directory.Exists(Path.Combine(WorkingDirectory, "objects")) ||
!File.Exists(Path.Combine(WorkingDirectory, "HEAD")))
return false;
var rs = ReadToEnd();
return rs.IsSuccess && rs.StdOut.Trim() == "true";
}
}
}

View file

@ -0,0 +1,17 @@
namespace SourceGit.Commands
{
public class IsCommitSHA : Command
{
public IsCommitSHA(string repo, string hash)
{
WorkingDirectory = repo;
Args = $"cat-file -t {hash}";
}
public bool Result()
{
var rs = ReadToEnd();
return rs.IsSuccess && rs.StdOut.Trim().Equals("commit");
}
}
}

View file

@ -7,7 +7,7 @@ namespace SourceGit.Commands
{
public partial class LFS
{
[GeneratedRegex(@"^(.+)\s+(\w+)\s+\w+:(\d+)$")]
[GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")]
private static partial Regex REG_LOCK();
class SubCmd : Command
@ -82,7 +82,7 @@ namespace SourceGit.Commands
var rs = cmd.ReadToEnd();
if (rs.IsSuccess)
{
var lines = rs.StdOut.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_LOCK().Match(line);

View file

@ -13,9 +13,12 @@ namespace SourceGit.Commands
cmd.Context = repo;
cmd.RaiseError = true;
// NOTE: If no <file> names are specified, 'git mergetool' will run the merge tool program on every file with merge conflicts.
var fileArg = string.IsNullOrEmpty(file) ? "" : $"\"{file}\"";
if (toolType == 0)
{
cmd.Args = $"mergetool \"{file}\"";
cmd.Args = $"mergetool {fileArg}";
return cmd.Exec();
}
@ -32,7 +35,7 @@ namespace SourceGit.Commands
return false;
}
cmd.Args = $"-c mergetool.sourcegit.cmd=\"\\\"{toolPath}\\\" {supported.Cmd}\" -c mergetool.writeToTemp=true -c mergetool.keepBackup=false -c mergetool.trustExitCode=true mergetool --tool=sourcegit \"{file}\"";
cmd.Args = $"-c mergetool.sourcegit.cmd=\"\\\"{toolPath}\\\" {supported.Cmd}\" -c mergetool.writeToTemp=true -c mergetool.keepBackup=false -c mergetool.trustExitCode=true mergetool --tool=sourcegit {fileArg}";
return cmd.Exec();
}

View file

@ -4,21 +4,20 @@ namespace SourceGit.Commands
{
public class Pull : Command
{
public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, bool prune, Action<string> outputHandler)
public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, Action<string> outputHandler)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args = "pull --verbose --progress --tags ";
Args = "pull --verbose --progress ";
if (useRebase)
Args += "--rebase ";
Args += "--rebase=true ";
if (noTags)
Args += "--no-tags ";
if (prune)
Args += "--prune ";
Args += $"{remote} {branch}";
}

View file

@ -26,7 +26,7 @@ namespace SourceGit.Commands
Args += $"{remote} {local}:{remoteBranch}";
}
public Push(string repo, string remote, string tag, bool isDelete)
public Push(string repo, string remote, string refname, bool isDelete)
{
WorkingDirectory = repo;
Context = repo;
@ -36,7 +36,7 @@ namespace SourceGit.Commands
if (isDelete)
Args += "--delete ";
Args += $"{remote} refs/tags/{tag}";
Args += $"{remote} {refname}";
}
protected override void OnReadline(string line)

View file

@ -24,12 +24,23 @@ namespace SourceGit.Commands
if (!rs.IsSuccess)
return branches;
var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
var remoteBranches = new HashSet<string>();
foreach (var line in lines)
{
var b = ParseLine(line);
if (b != null)
{
branches.Add(b);
if (!b.IsLocal)
remoteBranches.Add(b.FullName);
}
}
foreach (var b in branches)
{
if (b.IsLocal && !string.IsNullOrEmpty(b.Upstream))
b.IsUpsteamGone = !remoteBranches.Contains(b.Upstream);
}
return branches;
@ -75,6 +86,7 @@ namespace SourceGit.Commands
branch.Head = parts[1];
branch.IsCurrent = parts[2] == "*";
branch.Upstream = parts[3];
branch.IsUpsteamGone = false;
if (branch.IsLocal && !string.IsNullOrEmpty(parts[4]) && !parts[4].Equals("=", StringComparison.Ordinal))
branch.TrackStatus = new QueryTrackStatus(WorkingDirectory, branch.Name, branch.Upstream).Result();

View file

@ -9,10 +9,10 @@ namespace SourceGit.Commands
WorkingDirectory = repo;
Context = repo;
_commit = commit;
Args = $"rev-list -{max} --parents --branches --remotes ^{commit}";
Args = $"rev-list -{max} --parents --branches --remotes --ancestry-path ^{commit}";
}
public IEnumerable<string> Result()
public List<string> Result()
{
Exec();
return _lines;

View file

@ -6,7 +6,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = $"show --no-show-signature --pretty=format:%B -s {sha}";
Args = $"show --no-show-signature --format=%B -s {sha}";
}
public string Result()

View file

@ -7,7 +7,7 @@
WorkingDirectory = repo;
Context = repo;
const string baseArgs = "show --no-show-signature --pretty=format:\"%G?%n%GS%n%GK\" -s";
const string baseArgs = "show --no-show-signature --format=%G?%n%GS%n%GK -s";
const string fakeSignersFileArg = "-c gpg.ssh.allowedSignersFile=/dev/null";
Args = $"{(useFakeSignersFile ? fakeSignersFileArg : string.Empty)} {baseArgs} {sha}";
}
@ -18,7 +18,7 @@
if (!rs.IsSuccess)
return null;
var raw = rs.StdOut.Trim();
var raw = rs.StdOut.Trim().ReplaceLineEndings("\n");
if (raw.Length <= 1)
return null;
@ -29,7 +29,6 @@
Signer = lines[1],
Key = lines[2]
};
}
}
}

View file

@ -10,7 +10,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = $"log --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s {limits}";
Args = $"log --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s {limits}";
_findFirstMerged = needFindHead;
}
@ -18,9 +18,13 @@ namespace SourceGit.Commands
{
string search = onlyCurrentBranch ? string.Empty : "--branches --remotes ";
if (method == Models.CommitSearchMethod.ByUser)
if (method == Models.CommitSearchMethod.ByAuthor)
{
search += $"-i --author=\"{filter}\" --committer=\"{filter}\"";
search += $"-i --author=\"{filter}\"";
}
else if (method == Models.CommitSearchMethod.ByCommitter)
{
search += $"-i --committer=\"{filter}\"";
}
else if (method == Models.CommitSearchMethod.ByFile)
{
@ -31,7 +35,7 @@ namespace SourceGit.Commands
var argsBuilder = new StringBuilder();
argsBuilder.Append(search);
var words = filter.Split(new[] { ' ', '\t', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var words = filter.Split([' ', '\t', '\r'], StringSplitOptions.RemoveEmptyEntries);
foreach (var word in words)
{
var escaped = word.Trim().Replace("\"", "\\\"", StringComparison.Ordinal);
@ -44,7 +48,7 @@ namespace SourceGit.Commands
WorkingDirectory = repo;
Context = repo;
Args = $"log -1000 --date-order --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + search;
Args = $"log -1000 --date-order --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + search;
_findFirstMerged = false;
}
@ -120,7 +124,7 @@ namespace SourceGit.Commands
Args = $"log --since=\"{_commits[^1].CommitterTimeStr}\" --format=\"%H\"";
var rs = ReadToEnd();
var shas = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var shas = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
if (shas.Length == 0)
return;

View file

@ -3,18 +3,18 @@ using System.Collections.Generic;
namespace SourceGit.Commands
{
public class QueryCommitsWithFullMessage : Command
public class QueryCommitsForInteractiveRebase : Command
{
public QueryCommitsWithFullMessage(string repo, string args)
public QueryCommitsForInteractiveRebase(string repo, string on)
{
_boundary = $"----- BOUNDARY OF COMMIT {Guid.NewGuid()} -----";
WorkingDirectory = repo;
Context = repo;
Args = $"log --date-order --no-show-signature --decorate=full --pretty=format:\"%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%B%n{_boundary}\" {args}";
Args = $"log --date-order --no-show-signature --decorate=full --format=\"%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%B%n{_boundary}\" {on}..HEAD";
}
public List<Models.CommitWithMessage> Result()
public List<Models.InteractiveCommit> Result()
{
var rs = ReadToEnd();
if (!rs.IsSuccess)
@ -29,7 +29,7 @@ namespace SourceGit.Commands
switch (nextPartIdx)
{
case 0:
_current = new Models.CommitWithMessage();
_current = new Models.InteractiveCommit();
_current.Commit.SHA = line;
_commits.Add(_current);
break;
@ -52,7 +52,7 @@ namespace SourceGit.Commands
_current.Commit.CommitterTime = ulong.Parse(line);
break;
default:
var boundary = rs.StdOut.IndexOf(_boundary, end + 1);
var boundary = rs.StdOut.IndexOf(_boundary, end + 1, StringComparison.Ordinal);
if (boundary > end)
{
_current.Message = rs.StdOut.Substring(start, boundary - start - 1);
@ -88,8 +88,8 @@ namespace SourceGit.Commands
_current.Commit.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries));
}
private List<Models.CommitWithMessage> _commits = new List<Models.CommitWithMessage>();
private Models.CommitWithMessage _current = null;
private List<Models.InteractiveCommit> _commits = [];
private Models.InteractiveCommit _current = null;
private string _boundary = "";
}
}

View file

@ -0,0 +1,26 @@
using System.IO;
namespace SourceGit.Commands
{
public class QueryGitCommonDir : Command
{
public QueryGitCommonDir(string workDir)
{
WorkingDirectory = workDir;
Args = "rev-parse --git-common-dir";
RaiseError = false;
}
public string Result()
{
var rs = ReadToEnd().StdOut;
if (string.IsNullOrEmpty(rs))
return null;
rs = rs.Trim();
if (Path.IsPathRooted(rs))
return rs;
return Path.GetFullPath(Path.Combine(WorkingDirectory, rs));
}
}
}

View file

@ -13,7 +13,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = $"status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain";
Args = $"--no-optional-locks status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain";
}
public List<Models.Change> Result()

View file

@ -20,7 +20,7 @@ namespace SourceGit.Commands
if (!output.IsSuccess)
return rs;
var lines = output.StdOut.Split('\n');
var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.EndsWith("/HEAD", StringComparison.Ordinal))

View file

@ -0,0 +1,21 @@
namespace SourceGit.Commands
{
public class QueryRevisionByRefName : Command
{
public QueryRevisionByRefName(string repo, string refname)
{
WorkingDirectory = repo;
Context = repo;
Args = $"rev-parse {refname}";
}
public string Result()
{
var rs = ReadToEnd();
if (rs.IsSuccess && !string.IsNullOrEmpty(rs.StdOut))
return rs.StdOut.Trim();
return null;
}
}
}

View file

@ -1,4 +1,6 @@
namespace SourceGit.Commands
using System.Collections.Generic;
namespace SourceGit.Commands
{
public class QueryRevisionFileNames : Command
{
@ -9,13 +11,17 @@
Args = $"ls-tree -r -z --name-only {revision}";
}
public string[] Result()
public List<string> Result()
{
var rs = ReadToEnd();
if (rs.IsSuccess)
return rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries);
if (!rs.IsSuccess)
return [];
return [];
var lines = rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries);
var outs = new List<string>();
foreach (var line in lines)
outs.Add(line);
return outs;
}
}
}

View file

@ -8,7 +8,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = $"show --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s -s {sha}";
Args = $"show --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s -s {sha}";
}
public Models.Commit Result()

View file

@ -24,7 +24,7 @@ namespace SourceGit.Commands
if (rs.IsSuccess)
{
var changes = new List<Models.Change>();
var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_FORMAT2().Match(line);

View file

@ -1,60 +1,76 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
/// <summary>
/// Query stash changes. Requires git >= 2.32.0
/// </summary>
public partial class QueryStashChanges : Command
{
[GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")]
[GeneratedRegex(@"^([MADC])\s+(.+)$")]
private static partial Regex REG_FORMAT();
[GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)$")]
private static partial Regex REG_RENAME_FORMAT();
public QueryStashChanges(string repo, string sha)
public QueryStashChanges(string repo, string stash)
{
WorkingDirectory = repo;
Context = repo;
Args = $"diff --name-status --pretty=format: {sha}^ {sha}";
Args = $"stash show -u --name-status \"{stash}\"";
}
public List<Models.Change> Result()
{
Exec();
return _changes;
}
var rs = ReadToEnd();
if (!rs.IsSuccess)
return [];
protected override void OnReadline(string line)
{
var match = REG_FORMAT().Match(line);
if (!match.Success)
return;
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
switch (status[0])
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
var outs = new List<Models.Change>();
foreach (var line in lines)
{
case 'M':
change.Set(Models.ChangeState.Modified);
_changes.Add(change);
break;
case 'A':
change.Set(Models.ChangeState.Added);
_changes.Add(change);
break;
case 'D':
change.Set(Models.ChangeState.Deleted);
_changes.Add(change);
break;
case 'R':
change.Set(Models.ChangeState.Renamed);
_changes.Add(change);
break;
case 'C':
change.Set(Models.ChangeState.Copied);
_changes.Add(change);
break;
}
}
var match = REG_FORMAT().Match(line);
if (!match.Success)
{
match = REG_RENAME_FORMAT().Match(line);
if (match.Success)
{
var renamed = new Models.Change() { Path = match.Groups[1].Value };
renamed.Set(Models.ChangeState.Renamed);
outs.Add(renamed);
}
private readonly List<Models.Change> _changes = new List<Models.Change>();
continue;
}
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
switch (status[0])
{
case 'M':
change.Set(Models.ChangeState.Modified);
outs.Add(change);
break;
case 'A':
change.Set(Models.ChangeState.Added);
outs.Add(change);
break;
case 'D':
change.Set(Models.ChangeState.Deleted);
outs.Add(change);
break;
case 'C':
change.Set(Models.ChangeState.Copied);
outs.Add(change);
break;
}
}
outs.Sort((l, r) => string.Compare(l.Path, r.Path, StringComparison.Ordinal));
return outs;
}
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
namespace SourceGit.Commands
{
@ -8,7 +9,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = "stash list --pretty=format:%H%n%ct%n%gd%n%s";
Args = "stash list --format=%H%n%P%n%ct%n%gd%n%s";
}
public List<Models.Stash> Result()
@ -26,21 +27,32 @@ namespace SourceGit.Commands
_stashes.Add(_current);
break;
case 1:
_current.Time = ulong.Parse(line);
ParseParent(line);
break;
case 2:
_current.Name = line;
_current.Time = ulong.Parse(line);
break;
case 3:
_current.Name = line;
break;
case 4:
_current.Message = line;
break;
}
_nextLineIdx++;
if (_nextLineIdx > 3)
if (_nextLineIdx > 4)
_nextLineIdx = 0;
}
private void ParseParent(string data)
{
if (data.Length < 8)
return;
_current.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries));
}
private readonly List<Models.Stash> _stashes = new List<Models.Stash>();
private Models.Stash _current = null;
private int _nextLineIdx = 0;

View file

@ -24,11 +24,9 @@ namespace SourceGit.Commands
{
var submodules = new List<Models.Submodule>();
var rs = ReadToEnd();
if (!rs.IsSuccess)
return submodules;
var builder = new StringBuilder();
var lines = rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries);
var lines = rs.StdOut.Split(['\r', '\n'], System.StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_FORMAT1().Match(line);
@ -51,13 +49,13 @@ namespace SourceGit.Commands
if (submodules.Count > 0)
{
Args = $"status -uno --porcelain -- {builder}";
Args = $"--no-optional-locks status -uno --porcelain -- {builder}";
rs = ReadToEnd();
if (!rs.IsSuccess)
return submodules;
var dirty = new HashSet<string>();
lines = rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries);
lines = rs.StdOut.Split(['\r', '\n'], System.StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_FORMAT_STATUS().Match(line);

View file

@ -19,7 +19,7 @@ namespace SourceGit.Commands
if (!rs.IsSuccess)
return status;
var lines = rs.StdOut.Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line[0] == '>')

View file

@ -37,6 +37,19 @@ namespace SourceGit.Commands
return true;
}
public static bool ProcessStashChanges(string repo, List<Models.DiffOption> opts, string saveTo)
{
using (var sw = File.Create(saveTo))
{
foreach (var opt in opts)
{
if (!ProcessSingleChange(repo, opt, sw))
return false;
}
}
return true;
}
private static bool ProcessSingleChange(string repo, Models.DiffOption opt, FileStream writer)
{
var starter = new ProcessStartInfo();

View file

@ -11,16 +11,26 @@ namespace SourceGit.Commands
Context = repo;
}
public bool Push(string message)
public bool Push(string message, bool includeUntracked = true, bool keepIndex = false)
{
Args = $"stash push -m \"{message}\"";
var builder = new StringBuilder();
builder.Append("stash push ");
if (includeUntracked)
builder.Append("--include-untracked ");
if (keepIndex)
builder.Append("--keep-index ");
builder.Append("-m \"");
builder.Append(message);
builder.Append("\"");
Args = builder.ToString();
return Exec();
}
public bool Push(string message, List<Models.Change> changes, bool keepIndex)
{
var builder = new StringBuilder();
builder.Append("stash push ");
builder.Append("stash push --include-untracked ");
if (keepIndex)
builder.Append("--keep-index ");
builder.Append("-m \"");
@ -37,7 +47,7 @@ namespace SourceGit.Commands
public bool Push(string message, string pathspecFromFile, bool keepIndex)
{
var builder = new StringBuilder();
builder.Append("stash push --pathspec-from-file=\"");
builder.Append("stash push --include-untracked --pathspec-from-file=\"");
builder.Append(pathspecFromFile);
builder.Append("\" ");
if (keepIndex)
@ -63,21 +73,22 @@ namespace SourceGit.Commands
return Exec();
}
public bool Apply(string name)
public bool Apply(string name, bool restoreIndex)
{
Args = $"stash apply -q {name}";
var opts = restoreIndex ? "--index" : string.Empty;
Args = $"stash apply -q {opts} \"{name}\"";
return Exec();
}
public bool Pop(string name)
{
Args = $"stash pop -q {name}";
Args = $"stash pop -q --index \"{name}\"";
return Exec();
}
public bool Drop(string name)
{
Args = $"stash drop -q {name}";
Args = $"stash drop -q \"{name}\"";
return Exec();
}

View file

@ -8,7 +8,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = $"log --date-order --branches --remotes -{max} --pretty=format:\"%ct$%aN\"";
Args = $"log --date-order --branches --remotes -{max} --format=%ct$%aN±%aE";
}
public Models.Statistics Result()

View file

@ -48,9 +48,7 @@ namespace SourceGit.Commands
if (remotes != null)
{
foreach (var r in remotes)
{
new Push(repo, r.Name, name, true).Exec();
}
new Push(repo, r.Name, $"refs/tags/{name}", true).Exec();
}
return true;

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace SourceGit.Commands
{
@ -20,12 +21,13 @@ namespace SourceGit.Commands
var last = null as Models.Worktree;
if (rs.IsSuccess)
{
var lines = rs.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.StartsWith("worktree ", StringComparison.Ordinal))
{
last = new Models.Worktree() { FullPath = line.Substring(9).Trim() };
last.RelativePath = Path.GetRelativePath(WorkingDirectory, last.FullPath);
worktrees.Add(last);
}
else if (line.StartsWith("bare", StringComparison.Ordinal))
@ -73,6 +75,8 @@ namespace SourceGit.Commands
if (!string.IsNullOrEmpty(tracking))
Args += tracking;
else if (!string.IsNullOrEmpty(name) && !createNew)
Args += name;
_outputHandler = outputHandler;
return Exec();