diff --git a/README.md b/README.md index e922c476..67ee5258 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ ## Translation Status -[![en_US](https://img.shields.io/badge/en__US-100%25-brightgreen)](TRANSLATION.md) [![de__DE](https://img.shields.io/badge/de__DE-100.00%25-brightgreen)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-100.00%25-brightgreen)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-94.69%25-yellow)](TRANSLATION.md) [![it__IT](https://img.shields.io/badge/it__IT-93.32%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-94.41%25-yellow)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-100.00%25-brightgreen)](TRANSLATION.md) [![zh__CN](https://img.shields.io/badge/zh__CN-100.00%25-brightgreen)](TRANSLATION.md) [![zh__TW](https://img.shields.io/badge/zh__TW-100.00%25-brightgreen)](TRANSLATION.md) +[![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)](TRANSLATION.md) [![de__DE](https://img.shields.io/badge/de__DE-98.13%25-yellow)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-98.13%25-yellow)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-92.91%25-yellow)](TRANSLATION.md) [![it__IT](https://img.shields.io/badge/it__IT-98.40%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-92.65%25-yellow)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-98.13%25-yellow)](TRANSLATION.md) [![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen)](TRANSLATION.md) [![zh__TW](https://img.shields.io/badge/zh__TW-%E2%88%9A-brightgreen)](TRANSLATION.md) > [!NOTE] > You can find the missing keys in [TRANSLATION.md](TRANSLATION.md) @@ -136,11 +136,11 @@ This software supports using OpenAI or other AI service that has an OpenAI comap For `OpenAI`: -* `Server` must be `https://api.openai.com/v1/chat/completions` +* `Server` must be `https://api.openai.com/v1` For other AI service: -* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1/chat/completions`. For example, when using `Ollama`, it should be `http://localhost:11434/v1/chat/completions` instead of `http://localhost:11434/api/generate` +* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1`. For example, when using `Ollama`, it should be `http://localhost:11434/v1` instead of `http://localhost:11434/api/generate` * The `API Key` is optional that depends on the service ## External Tools diff --git a/TRANSLATION.md b/TRANSLATION.md index aa42d82b..9f7a1c80 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -1,29 +1,66 @@ -### de_DE.axaml: 100.00% +### de_DE.axaml: 98.13%
Missing Keys - +- Text.AIAssistant.Regen +- Text.AIAssistant.Use +- Text.ApplyStash +- Text.ApplyStash.DropAfterApply +- Text.ApplyStash.RestoreIndex +- Text.ApplyStash.Stash +- Text.Clone.RecurseSubmodules +- Text.CreateBranch.Name.WarnSpace +- Text.DeleteRepositoryNode.Path +- Text.DeleteRepositoryNode.TipForGroup +- Text.DeleteRepositoryNode.TipForRepository +- Text.Stash.AutoRestore +- Text.Stash.AutoRestore.Tip +- Text.WorkingCopy.SignOff
-### es_ES.axaml: 100.00% +### es_ES.axaml: 98.13%
Missing Keys - +- Text.AIAssistant.Regen +- Text.AIAssistant.Use +- Text.ApplyStash +- Text.ApplyStash.DropAfterApply +- Text.ApplyStash.RestoreIndex +- Text.ApplyStash.Stash +- Text.Clone.RecurseSubmodules +- Text.CreateBranch.Name.WarnSpace +- Text.DeleteRepositoryNode.Path +- Text.DeleteRepositoryNode.TipForGroup +- Text.DeleteRepositoryNode.TipForRepository +- Text.Stash.AutoRestore +- Text.Stash.AutoRestore.Tip +- Text.WorkingCopy.SignOff
-### fr_FR.axaml: 94.69% +### fr_FR.axaml: 92.91%
Missing Keys +- Text.AIAssistant.Regen +- Text.AIAssistant.Use +- Text.ApplyStash +- Text.ApplyStash.DropAfterApply +- Text.ApplyStash.RestoreIndex +- Text.ApplyStash.Stash +- Text.Clone.RecurseSubmodules +- Text.CreateBranch.Name.WarnSpace +- Text.DeleteRepositoryNode.Path +- Text.DeleteRepositoryNode.TipForGroup +- Text.DeleteRepositoryNode.TipForRepository - Text.InProgress.CherryPick.Head - Text.InProgress.Merge.Operating - Text.InProgress.Rebase.StoppedAt @@ -62,81 +99,58 @@ - Text.SetUpstream.Unset - Text.SetUpstream.Upstream - Text.SHALinkCM.NavigateTo +- Text.Stash.AutoRestore +- Text.Stash.AutoRestore.Tip - Text.WorkingCopy.CommitToEdit +- Text.WorkingCopy.SignOff
-### it_IT.axaml: 93.32% - - -
-Missing Keys - -- Text.BranchCM.MergeMultiBranches -- Text.CommitCM.Merge -- Text.CommitCM.MergeMultiple -- Text.CommitDetail.Files.Search -- Text.CommitDetail.Info.Children -- Text.Configure.IssueTracker.AddSampleGiteeIssue -- Text.Configure.IssueTracker.AddSampleGiteePullRequest -- Text.Configure.IssueTracker.AddSampleGitLabMergeRequest -- Text.Configure.OpenAI.Preferred -- Text.Configure.OpenAI.Preferred.Tip -- Text.Diff.UseBlockNavigation -- Text.Fetch.Force -- Text.FileCM.ResolveUsing -- Text.InProgress.CherryPick.Head -- Text.InProgress.Merge.Operating -- Text.InProgress.Rebase.StoppedAt -- Text.InProgress.Revert.Head -- Text.Merge.Source -- Text.MergeMultiple -- Text.MergeMultiple.CommitChanges -- Text.MergeMultiple.Strategy -- Text.MergeMultiple.Targets -- Text.Preferences.General.DateFormat -- Text.Preferences.General.ShowChildren -- Text.Preferences.Git.SSLVerify -- Text.Repository.FilterCommits -- Text.Repository.FilterCommits.Default -- Text.Repository.FilterCommits.Exclude -- Text.Repository.FilterCommits.Include -- Text.Repository.HistoriesLayout -- Text.Repository.HistoriesLayout.Horizontal -- Text.Repository.HistoriesLayout.Vertical -- Text.Repository.HistoriesOrder -- Text.Repository.HistoriesOrder.ByDate -- Text.Repository.HistoriesOrder.Topo -- Text.Repository.OnlyHighlightCurrentBranchInHistories -- Text.Repository.Skip -- Text.Repository.Tags.OrderByCreatorDate -- Text.Repository.Tags.OrderByNameAsc -- Text.Repository.Tags.OrderByNameDes -- Text.Repository.Tags.Sort -- Text.Repository.UseRelativeTimeInHistories -- Text.SetUpstream -- Text.SetUpstream.Local -- Text.SetUpstream.Unset -- Text.SetUpstream.Upstream -- Text.SHALinkCM.CopySHA -- Text.SHALinkCM.NavigateTo -- Text.WorkingCopy.CommitToEdit - -
- -### pt_BR.axaml: 94.41% +### it_IT.axaml: 98.40%
Missing Keys +- Text.AIAssistant.Regen +- Text.AIAssistant.Use +- Text.ApplyStash +- Text.ApplyStash.DropAfterApply +- Text.ApplyStash.RestoreIndex +- Text.ApplyStash.Stash +- Text.Clone.RecurseSubmodules +- Text.DeleteRepositoryNode.Path +- Text.DeleteRepositoryNode.TipForGroup +- Text.DeleteRepositoryNode.TipForRepository +- Text.Stash.AutoRestore +- Text.Stash.AutoRestore.Tip + +
+ +### pt_BR.axaml: 92.65% + + +
+Missing Keys + +- Text.AIAssistant.Regen +- Text.AIAssistant.Use +- Text.ApplyStash +- Text.ApplyStash.DropAfterApply +- Text.ApplyStash.RestoreIndex +- Text.ApplyStash.Stash - Text.BranchCM.MergeMultiBranches +- Text.Clone.RecurseSubmodules - Text.CommitCM.Merge - Text.CommitCM.MergeMultiple - Text.CommitDetail.Files.Search - Text.CommitDetail.Info.Children - Text.Configure.IssueTracker.AddSampleGiteeIssue - Text.Configure.IssueTracker.AddSampleGiteePullRequest +- Text.CreateBranch.Name.WarnSpace +- Text.DeleteRepositoryNode.Path +- Text.DeleteRepositoryNode.TipForGroup +- Text.DeleteRepositoryNode.TipForRepository - Text.Diff.UseBlockNavigation - Text.Fetch.Force - Text.FileCM.ResolveUsing @@ -170,17 +184,33 @@ - Text.SetUpstream.Unset - Text.SetUpstream.Upstream - Text.SHALinkCM.NavigateTo +- Text.Stash.AutoRestore +- Text.Stash.AutoRestore.Tip - Text.WorkingCopy.CommitToEdit +- Text.WorkingCopy.SignOff
-### ru_RU.axaml: 100.00% +### ru_RU.axaml: 98.13%
Missing Keys - +- Text.AIAssistant.Regen +- Text.AIAssistant.Use +- Text.ApplyStash +- Text.ApplyStash.DropAfterApply +- Text.ApplyStash.RestoreIndex +- Text.ApplyStash.Stash +- Text.Clone.RecurseSubmodules +- Text.CreateBranch.Name.WarnSpace +- Text.DeleteRepositoryNode.Path +- Text.DeleteRepositoryNode.TipForGroup +- Text.DeleteRepositoryNode.TipForRepository +- Text.Stash.AutoRestore +- Text.Stash.AutoRestore.Tip +- Text.WorkingCopy.SignOff
diff --git a/VERSION b/VERSION index d5cfddc3..4ee298df 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2025.03 \ No newline at end of file +2025.04 \ No newline at end of file diff --git a/build/scripts/localization-check.js b/build/scripts/localization-check.js index 45db82be..ed89a5e8 100644 --- a/build/scripts/localization-check.js +++ b/build/scripts/localization-check.js @@ -25,7 +25,7 @@ async function calculateTranslationRate() { const files = (await fs.readdir(localesDir)).filter(file => file !== 'en_US.axaml' && file.endsWith('.axaml')); // Add en_US badge first - badges.push(`[![en_US](https://img.shields.io/badge/en__US-100%25-brightgreen)](TRANSLATION.md)`); + badges.push(`[![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)](TRANSLATION.md)`); for (const file of files) { const filePath = path.join(localesDir, file); @@ -40,8 +40,12 @@ async function calculateTranslationRate() { // Add badges const locale = file.replace('.axaml', '').replace('_', '__'); - const badgeColor = translationRate === 100 ? 'brightgreen' : translationRate >= 75 ? 'yellow' : 'red'; - badges.push(`[![${locale}](https://img.shields.io/badge/${locale}-${translationRate.toFixed(2)}%25-${badgeColor})](TRANSLATION.md)`); + if (translationRate === 100) { + badges.push(`[![${locale}](https://img.shields.io/badge/${locale}-%E2%88%9A-brightgreen)](TRANSLATION.md)`); + } else { + const badgeColor = translationRate >= 75 ? 'yellow' : 'red'; + badges.push(`[![${locale}](https://img.shields.io/badge/${locale}-${translationRate.toFixed(2)}%25-${badgeColor})](TRANSLATION.md)`); + } } console.log(translationRates.join('\n\n')); diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs index f37e269c..9cad0792 100644 --- a/src/App.JsonCodeGen.cs +++ b/src/App.JsonCodeGen.cs @@ -46,8 +46,6 @@ namespace SourceGit [JsonSerializable(typeof(Models.ExternalToolPaths))] [JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))] [JsonSerializable(typeof(Models.JetBrainsState))] - [JsonSerializable(typeof(Models.OpenAIChatRequest))] - [JsonSerializable(typeof(Models.OpenAIChatResponse))] [JsonSerializable(typeof(Models.ThemeOverrides))] [JsonSerializable(typeof(Models.Version))] [JsonSerializable(typeof(Models.RepositorySettings))] diff --git a/src/App.axaml.cs b/src/App.axaml.cs index cca9f2ea..95b396de 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Net.Http; using System.Reflection; using System.Text; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Avalonia; @@ -332,17 +334,16 @@ namespace SourceGit builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n"); builder.Append("----------------------------\n"); builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n"); - builder.Append($"OS: {Environment.OSVersion.ToString()}\n"); + builder.Append($"OS: {Environment.OSVersion}\n"); builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n"); builder.Append($"Source: {ex.Source}\n"); + builder.Append($"Thread Name: {Thread.CurrentThread.Name ?? "Unnamed"}\n"); + builder.Append($"User: {Environment.UserName}\n"); + builder.Append($"App Start Time: {Process.GetCurrentProcess().StartTime}\n"); + builder.Append($"Exception Time: {DateTime.Now}\n"); + builder.Append($"Memory Usage: {Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024} MB\n"); builder.Append($"---------------------------\n\n"); - builder.Append(ex.StackTrace); - while (ex.InnerException != null) - { - ex = ex.InnerException; - builder.Append($"\n\nInnerException::: {ex.GetType().FullName}: {ex.Message}\n"); - builder.Append(ex.StackTrace); - } + builder.Append(ex); var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log"); diff --git a/src/Commands/Clone.cs b/src/Commands/Clone.cs index 683b8846..f2faed14 100644 --- a/src/Commands/Clone.cs +++ b/src/Commands/Clone.cs @@ -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} "; diff --git a/src/Commands/CompareRevisions.cs b/src/Commands/CompareRevisions.cs index c4674c8e..8a6f2832 100644 --- a/src/Commands/CompareRevisions.cs +++ b/src/Commands/CompareRevisions.cs @@ -18,6 +18,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 Result() { Exec(); diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs index 1c3e78cb..06ae8cb6 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -4,7 +4,7 @@ namespace SourceGit.Commands { public class Fetch : Command { - public Fetch(string repo, string remote, bool noTags, bool prune, bool force, Action outputHandler) + public Fetch(string repo, string remote, bool noTags, bool force, Action outputHandler) { _outputHandler = outputHandler; WorkingDirectory = repo; @@ -21,9 +21,6 @@ namespace SourceGit.Commands if (force) Args += "--force "; - if (prune) - Args += "--prune "; - Args += remote; } diff --git a/src/Commands/GenerateCommitMessage.cs b/src/Commands/GenerateCommitMessage.cs index e4f25f38..4b18a561 100644 --- a/src/Commands/GenerateCommitMessage.cs +++ b/src/Commands/GenerateCommitMessage.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; using System.Text; +using System.Text.RegularExpressions; using System.Threading; +using Avalonia.Threading; + namespace SourceGit.Commands { /// @@ -20,82 +23,134 @@ namespace SourceGit.Commands } } - public GenerateCommitMessage(Models.OpenAIService service, string repo, List changes, CancellationToken cancelToken, Action onProgress) + public GenerateCommitMessage(Models.OpenAIService service, string repo, List changes, CancellationToken cancelToken, Action 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(); + 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) + { + var hasFirstValidChar = false; + var thinkingBuffer = new StringBuilder(); + _service.Chat( + _service.AnalyzeDiffPrompt, + $"Here is the `git diff` output: {rs.StdOut}", + _cancelToken, + update => + ProcessChatResponse(update, ref hasFirstValidChar, thinkingBuffer, + (responseBuilder, text => + _onResponse?.Invoke( + $"Waiting for pre-file analyzing to completed...\n\n{text}")), + (summaryBuilder, null))); + } - bodyBuilder.Append("- "); - bodyBuilder.Append(summary); - bodyBuilder.AppendLine(); + 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(); + var hasSubjectFirstValidChar = false; + var subjectThinkingBuffer = new StringBuilder(); + _service.Chat( + _service.GenerateSubjectPrompt, + $"Here are the summaries changes:\n{summaryBuilder}", + _cancelToken, + update => + ProcessChatResponse(update, ref hasSubjectFirstValidChar, subjectThinkingBuffer, + (subjectBuilder, text => _onResponse?.Invoke($"{text}\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) + private void ProcessChatResponse( + string update, + ref bool hasFirstValidChar, + StringBuilder thinkingBuffer, + params (StringBuilder builder, Action callback)[] outputs) { - var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd(); - var diff = rs.IsSuccess ? rs.StdOut : "unknown change"; + if (!hasFirstValidChar) + { + update = update.TrimStart(); + if (string.IsNullOrEmpty(update)) + return; + if (update.StartsWith("<", StringComparison.Ordinal)) + thinkingBuffer.Append(update); + hasFirstValidChar = true; + } - 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; + if (thinkingBuffer.Length > 0) + thinkingBuffer.Append(update); - return string.Empty; - } + if (thinkingBuffer.Length > 15) + { + var match = REG_COT.Match(thinkingBuffer.ToString()); + if (match.Success) + { + update = REG_COT.Replace(thinkingBuffer.ToString(), "").TrimStart(); + if (update.Length > 0) + { + foreach (var output in outputs) + output.builder.Append(update); + thinkingBuffer.Clear(); + } + return; + } - 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; + match = REG_THINK_START.Match(thinkingBuffer.ToString()); + if (!match.Success) + { + foreach (var output in outputs) + output.builder.Append(thinkingBuffer); + thinkingBuffer.Clear(); + return; + } + } - return string.Empty; + if (thinkingBuffer.Length == 0) + { + foreach (var output in outputs) + { + output.builder.Append(update); + output.callback?.Invoke(output.builder.ToString()); + } + } } private Models.OpenAIService _service; private string _repo; private List _changes; private CancellationToken _cancelToken; - private Action _onProgress; + private Action _onResponse; + + private static readonly Regex REG_COT = new(@"^<(think|thought|thinking|thought_chain)>(.*?)", RegexOptions.Singleline); + private static readonly Regex REG_THINK_START = new(@"^<(think|thought|thinking|thought_chain)>", RegexOptions.Singleline); } } diff --git a/src/Commands/Pull.cs b/src/Commands/Pull.cs index 732530f5..a4efa4b6 100644 --- a/src/Commands/Pull.cs +++ b/src/Commands/Pull.cs @@ -4,7 +4,7 @@ namespace SourceGit.Commands { public class Pull : Command { - public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, bool prune, Action outputHandler) + public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, Action outputHandler) { _outputHandler = outputHandler; WorkingDirectory = repo; @@ -17,8 +17,6 @@ namespace SourceGit.Commands Args += "--rebase "; if (noTags) Args += "--no-tags "; - if (prune) - Args += "--prune "; Args += $"{remote} {branch}"; } diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 6318f331..312c068f 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -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) { diff --git a/src/Commands/QuerySubmodules.cs b/src/Commands/QuerySubmodules.cs index 5fd6e3d5..6ebfa8b1 100644 --- a/src/Commands/QuerySubmodules.cs +++ b/src/Commands/QuerySubmodules.cs @@ -24,8 +24,6 @@ namespace SourceGit.Commands { var submodules = new List(); var rs = ReadToEnd(); - if (!rs.IsSuccess) - return submodules; var builder = new StringBuilder(); var lines = rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries); diff --git a/src/Commands/Stash.cs b/src/Commands/Stash.cs index 1cbf4b2a..7acfdf38 100644 --- a/src/Commands/Stash.cs +++ b/src/Commands/Stash.cs @@ -30,7 +30,7 @@ namespace SourceGit.Commands public bool Push(string message, List 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 \""); @@ -47,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) @@ -73,21 +73,22 @@ namespace SourceGit.Commands return Exec(); } - public bool Apply(string name) + public bool Apply(string name, bool restoreIndex) { - Args = $"stash apply --index -q {name}"; + var opts = restoreIndex ? "--index" : string.Empty; + Args = $"stash apply -q {opts} \"{name}\""; return Exec(); } public bool Pop(string name) { - Args = $"stash pop --index -q {name}"; + Args = $"stash pop -q \"{name}\""; return Exec(); } public bool Drop(string name) { - Args = $"stash drop -q {name}"; + Args = $"stash drop -q \"{name}\""; return Exec(); } diff --git a/src/Commands/Worktree.cs b/src/Commands/Worktree.cs index 7516b1e3..27c0e28e 100644 --- a/src/Commands/Worktree.cs +++ b/src/Commands/Worktree.cs @@ -73,6 +73,8 @@ namespace SourceGit.Commands if (!string.IsNullOrEmpty(tracking)) Args += tracking; + else if (!string.IsNullOrEmpty(name) && !createNew) + Args += name; _outputHandler = outputHandler; return Exec(); diff --git a/src/Converters/StringConverters.cs b/src/Converters/StringConverters.cs index e6f4237c..5e4608c5 100644 --- a/src/Converters/StringConverters.cs +++ b/src/Converters/StringConverters.cs @@ -78,5 +78,8 @@ namespace SourceGit.Converters return v.Substring(13); return v; }); + + public static readonly FuncValueConverter ContainsSpaces = + new FuncValueConverter(v => v != null && v.Contains(' ')); } } diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 9bc7f0c3..f015130a 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -8,7 +8,8 @@ namespace SourceGit.Models { public enum CommitSearchMethod { - ByUser, + ByAuthor, + ByCommitter, ByMessage, ByFile, } diff --git a/src/Models/OpenAI.cs b/src/Models/OpenAI.cs index df67ff66..a6648c11 100644 --- a/src/Models/OpenAI.cs +++ b/src/Models/OpenAI.cs @@ -1,81 +1,13 @@ using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; +using System.ClientModel; using System.Threading; - +using Azure.AI.OpenAI; using CommunityToolkit.Mvvm.ComponentModel; +using OpenAI; +using OpenAI.Chat; namespace SourceGit.Models { - public class OpenAIChatMessage - { - [JsonPropertyName("role")] - public string Role - { - get; - set; - } - - [JsonPropertyName("content")] - public string Content - { - get; - set; - } - } - - public class OpenAIChatChoice - { - [JsonPropertyName("index")] - public int Index - { - get; - set; - } - - [JsonPropertyName("message")] - public OpenAIChatMessage Message - { - get; - set; - } - } - - public class OpenAIChatResponse - { - [JsonPropertyName("choices")] - public List Choices - { - get; - set; - } = []; - } - - public class OpenAIChatRequest - { - [JsonPropertyName("model")] - public string Model - { - get; - set; - } - - [JsonPropertyName("messages")] - public List Messages - { - get; - set; - } = []; - - public void AddMessage(string role, string content) - { - Messages.Add(new OpenAIChatMessage { Role = role, Content = content }); - } - } - public class OpenAIService : ObservableObject { public string Name @@ -87,7 +19,15 @@ namespace SourceGit.Models public string Server { get => _server; - set => SetProperty(ref _server, value); + set + { + // migrate old server value + if (!string.IsNullOrEmpty(value) && value.EndsWith("/chat/completions", StringComparison.Ordinal)) + { + value = value.Substring(0, value.Length - "/chat/completions".Length); + } + SetProperty(ref _server, value); + } } public string ApiKey @@ -147,45 +87,39 @@ namespace SourceGit.Models """; } - public OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation) + public void Chat(string prompt, string question, CancellationToken cancellation, Action onUpdate) { - var chat = new OpenAIChatRequest() { Model = Model }; - chat.AddMessage("user", prompt); - chat.AddMessage("user", question); - - var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(60) }; - if (!string.IsNullOrEmpty(ApiKey)) + Uri server = new(Server); + ApiKeyCredential key = new(ApiKey); + ChatClient client = null; + if (Server.Contains("openai.azure.com/", StringComparison.Ordinal)) { - if (Server.Contains("openai.azure.com/", StringComparison.Ordinal)) - client.DefaultRequestHeaders.Add("api-key", ApiKey); - else - client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}"); + var azure = new AzureOpenAIClient(server, key); + client = azure.GetChatClient(Model); + } + else + { + var openai = new OpenAIClient(key, new() { Endpoint = server }); + client = openai.GetChatClient(Model); } - var req = new StringContent(JsonSerializer.Serialize(chat, JsonCodeGen.Default.OpenAIChatRequest), Encoding.UTF8, "application/json"); try { - var task = client.PostAsync(Server, req, cancellation); - task.Wait(cancellation); + var updates = client.CompleteChatStreaming([ + _model.Equals("o1-mini", StringComparison.Ordinal) ? new UserChatMessage(prompt) : new SystemChatMessage(prompt), + new UserChatMessage(question), + ], null, cancellation); - var rsp = task.Result; - var reader = rsp.Content.ReadAsStringAsync(cancellation); - reader.Wait(cancellation); - - var body = reader.Result; - if (!rsp.IsSuccessStatusCode) + foreach (var update in updates) { - throw new Exception($"AI service returns error code {rsp.StatusCode}. Body: {body ?? string.Empty}"); + if (update.ContentUpdate.Count > 0) + onUpdate.Invoke(update.ContentUpdate[0].Text); } - - return JsonSerializer.Deserialize(reader.Result, JsonCodeGen.Default.OpenAIChatResponse); } catch { - if (cancellation.IsCancellationRequested) - return null; - - throw; + if (!cancellation.IsCancellationRequested) + throw; } } diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs index 3c452460..dbe8cfa7 100644 --- a/src/Models/Remote.cs +++ b/src/Models/Remote.cs @@ -10,10 +10,10 @@ namespace SourceGit.Models private static partial Regex REG_HTTPS(); [GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-/~%]+/[\w\-\.%]+(\.git)?$")] private static partial Regex REG_SSH1(); - [GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/~]+/[\w\-\.]+(\.git)?$")] + [GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/~%]+/[\w\-\.%]+(\.git)?$")] private static partial Regex REG_SSH2(); - [GeneratedRegex(@"^git@([\w\.\-]+):([\w\-/~]+/[\w\-\.]+)\.git$")] + [GeneratedRegex(@"^git@([\w\.\-]+):([\w\-/~%]+/[\w\-\.%]+)\.git$")] private static partial Regex REG_TO_VISIT_URL_CAPTURE(); private static readonly Regex[] URL_FORMATS = [ diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs index 1d0b3c10..556c99ea 100644 --- a/src/Models/RepositorySettings.cs +++ b/src/Models/RepositorySettings.cs @@ -56,12 +56,6 @@ namespace SourceGit.Models set; } = DealWithLocalChanges.DoNothing; - public bool EnablePruneOnFetch - { - get; - set; - } = false; - public bool EnableForceOnFetch { get; @@ -188,6 +182,12 @@ namespace SourceGit.Models set; } = false; + public bool AutoRestoreAfterStash + { + get; + set; + } = false; + public string PreferedOpenAIService { get; diff --git a/src/Models/ShellOrTerminal.cs b/src/Models/ShellOrTerminal.cs index 1decdcfa..4f0222e8 100644 --- a/src/Models/ShellOrTerminal.cs +++ b/src/Models/ShellOrTerminal.cs @@ -42,6 +42,7 @@ namespace SourceGit.Models new ShellOrTerminal("mac-terminal", "Terminal", ""), new ShellOrTerminal("iterm2", "iTerm", ""), new ShellOrTerminal("warp", "Warp", ""), + new ShellOrTerminal("ghostty", "Ghostty", "") }; } else diff --git a/src/Models/TemplateEngine.cs b/src/Models/TemplateEngine.cs index 6b5f525d..8472750c 100644 --- a/src/Models/TemplateEngine.cs +++ b/src/Models/TemplateEngine.cs @@ -313,7 +313,7 @@ namespace SourceGit.Models private static bool IsNameChar(char c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; } // (?) notice or log if variable is not found diff --git a/src/Native/MacOS.cs b/src/Native/MacOS.cs index c9e6abad..633ef5eb 100644 --- a/src/Native/MacOS.cs +++ b/src/Native/MacOS.cs @@ -44,6 +44,8 @@ namespace SourceGit.Native return "iTerm"; case "warp": return "Warp"; + case "ghostty": + return "Ghostty"; } return string.Empty; diff --git a/src/Native/OS.cs b/src/Native/OS.cs index 177bbf9f..3a688654 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -25,7 +25,8 @@ namespace SourceGit.Native void OpenWithDefaultEditor(string file); } - public static string DataDir { + public static string DataDir + { get; private set; } = string.Empty; @@ -61,12 +62,14 @@ namespace SourceGit.Native private set; } = new Version(0, 0, 0); - public static string ShellOrTerminal { + public static string ShellOrTerminal + { get; set; } = string.Empty; - public static List ExternalTools { + public static List ExternalTools + { get; set; } = []; diff --git a/src/Native/Windows.cs b/src/Native/Windows.cs index 10f2970a..11b6bd13 100644 --- a/src/Native/Windows.cs +++ b/src/Native/Windows.cs @@ -152,7 +152,7 @@ namespace SourceGit.Native public void OpenBrowser(string url) { - var info = new ProcessStartInfo("cmd", $"/c start {url}"); + var info = new ProcessStartInfo("cmd", $"/c start \"\" \"{url}\""); info.CreateNoWindow = true; Process.Start(info); } diff --git a/src/Resources/Images/ShellIcons/ghostty.png b/src/Resources/Images/ShellIcons/ghostty.png new file mode 100644 index 00000000..e394a517 Binary files /dev/null and b/src/Resources/Images/ShellIcons/ghostty.png differ diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 5143fc37..fe961d14 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -160,8 +160,6 @@ Remotes automatisch fetchen Minute(n) Standard Remote - Aktivere --prune beim fetchen - Aktiviere --signoff für Commits TICKETSYSTEM Beispiel für Gitee Issue Regel einfügen Beispiel für Gitee Pull Request Regel einfügen @@ -478,6 +476,7 @@ Klon Standardordner Benutzer Email Globale Git Benutzer Email + Aktivere --prune beim fetchen Installationspfad Aktiviere HTTP SSL Verifizierung Benutzername @@ -570,8 +569,8 @@ Horizontal Vertikal COMMIT SORTIERUNG - Commit Zeitpunkt (--date-order) - Topologie (--topo-order) + Commit Zeitpunkt + Topologie LOKALE BRANCHES Zum HEAD wechseln Aktiviere '--first-parent' Option @@ -583,10 +582,11 @@ REMOTES REMOTE HINZUFÜGEN Commit suchen + Autor + Committer Dateiname Commit-Nachricht SHA - Autor & Committer Aktueller Branch Zeige Tags als Baum ÜBERSPRINGEN diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index d06f438d..018b54d7 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -19,7 +19,9 @@ Track Branch: Tracking remote branch AI Assistant + RE-GENERATE Use AI to generate commit message + APPLY AS COMMIT MESSAGE Patch Error Raise errors and refuses to apply the patch @@ -34,6 +36,10 @@ Warn Outputs warnings for a few such errors, but applies Whitespace: + Apply Stash + Delete after applying + Reinstate the index's changes + Stash: Archive... Save Archive To: Select archive file path @@ -97,6 +103,7 @@ Local Name: Repository name. Optional. Parent Folder: + Initialize & update submodules Repository URL: CLOSE Editor @@ -157,8 +164,6 @@ Fetch remotes automatically Minute(s) Default Remote - Enable --prune on fetch - Enable --signoff for commit ISSUE TRACKER Add Sample Gitee Issue Rule Add Sample Gitee Pull Request Rule @@ -201,6 +206,7 @@ Stash & Reapply New Branch Name: Enter branch name. + Spaces will be replaced with dashes. Create Local Branch Create Tag... New Tag At: @@ -224,8 +230,11 @@ You are trying to delete multiple branches at one time. Be sure to double-check before taking action! Delete Remote Remote: + Path: Target: + All children will be removed from list. Confirm Deleting Group + This will only remove it from list, not from disk! Confirm Deleting Repository Delete Submodule Submodule Path: @@ -273,7 +282,7 @@ Fast-Forward (without checkout) Fetch Fetch all remotes - Override refs check + Force override local refs Fetch without tags Remote: Fetch Remote Changes @@ -475,6 +484,7 @@ Default Clone Dir User Email Global git user email + Enable --prune on fetch Install Path Enable HTTP SSL Verify User Name @@ -567,8 +577,8 @@ Horizontal Vertical COMMITS ORDER - Commit Date (--date-order) - Topologically (--topo-order) + Commit Date + Topologically LOCAL BRANCHES Navigate to HEAD Enable '--first-parent' Option @@ -580,10 +590,11 @@ REMOTES ADD REMOTE Search Commit + Author + Committer File Message SHA - Author & Committer Current Branch Show Tags as Tree SKIP @@ -638,6 +649,8 @@ Private SSH key store path START Stash + Auto-restore after stashing + Your working files remain unchanged, but a stash is saved. Include untracked files Keep staged files Message: @@ -717,6 +730,7 @@ INCLUDE UNTRACKED FILES NO RECENT INPUT MESSAGES NO COMMIT TEMPLATES + SignOff STAGED UNSTAGE UNSTAGE ALL diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index 74b2f224..3e926c63 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -160,8 +160,6 @@ Fetch remotos automáticamente Minuto(s) Remoto por Defecto - Habilitar --prune para fetch - Habilitar --signoff para commit SEGUIMIENTO DE INCIDENCIAS Añadir Regla de Ejemplo para Incidencias de Gitee Añadir Regla de Ejemplo para Pull Requests de Gitee @@ -479,6 +477,7 @@ Directorio de clonado por defecto Email de usuario Email global del usuario git + Habilitar --prune para fetch Ruta de instalación Habilitar verificación HTTP SSL Nombre de usuario @@ -571,8 +570,8 @@ Horizontal Vertical ORDEN DE COMMITS - Fecha de Commit (--date-order) - Topológicamente (--topo-order) + Fecha de Commit + Topológicamente RAMAS LOCALES Navegar a HEAD Habilitar Opción '--first-parent' @@ -584,10 +583,11 @@ REMOTOS AÑADIR REMOTO Buscar Commit + Autor + Committer Archivo Mensaje SHA - Autor & Committer Rama Actual Mostrar Etiquetas como Árbol OMITIR @@ -707,7 +707,7 @@ Ignorar archivos *{0} en la misma carpeta Ignorar archivos en la misma carpeta Ignorar solo este archivo - Enmendar (Amend) + Enmendar Puedes stagear este archivo ahora. COMMIT COMMIT & PUSH diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index e4bb9c26..da20a5ee 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -161,8 +161,6 @@ Fetch les dépôts distants automatiquement minute(s) Dépôt par défaut - Activer --prune pour fetch - Activer --signoff pour commit SUIVI DES PROBLÈMES Ajouter une règle d'exemple Gitee Ajouter une règle d'exemple pour Pull Request Gitee @@ -467,6 +465,7 @@ Répertoire de clônage par défaut E-mail utilsateur E-mail utilsateur global + Activer --prune pour fetch Chemin d'installation Nom d'utilisateur Nom d'utilisateur global @@ -560,10 +559,11 @@ DEPOTS DISTANTS AJOUTER DEPOT DISTANT Rechercher un commit + Auteur + Committer Fichier Message SHA - Auteur & Committer Branche actuelle Voir les Tags en tant qu'arbre Statistiques diff --git a/src/Resources/Locales/it_IT.axaml b/src/Resources/Locales/it_IT.axaml index 72e2aa28..95f7bd1d 100644 --- a/src/Resources/Locales/it_IT.axaml +++ b/src/Resources/Locales/it_IT.axaml @@ -12,7 +12,7 @@ • Il codice sorgente è disponibile su Client GUI Git open source e gratuito Aggiungi Worktree - Cosa fare il checkout: + Di cosa fare il checkout: Branch esistente Crea nuovo branch Posizione: @@ -60,8 +60,9 @@ Recupera ${0}$ in ${1}$... Git Flow - Completa ${0}$ Unisci ${0}$ in ${1}$... - Recupera ${0}$ - Recupera ${0}$ in ${1}$... + Unisci i {0} branch selezionati in quello corrente + Scarica ${0}$ + Scarica ${0}$ in ${1}$... Invia ${0}$ Riallinea ${0}$ su ${1}$... Rinomina ${0}$... @@ -73,7 +74,7 @@ Ripristina la Revisione Padre Genera messaggio di commit CAMBIA MODALITÀ DI VISUALIZZAZIONE - Mostra come elenco di file e directory + Mostra come elenco di file e cartelle Mostra come elenco di percorsi Mostra come albero del filesystem Checkout Branch @@ -84,13 +85,13 @@ Modifiche Locali: Scarta Non fare nulla - Stash e Ripristina + Stasha e Ripristina Cherry Pick Aggiungi sorgente al messaggio di commit Commit(s): Conferma tutte le modifiche Mainline: - Di solito non è possibile cherry-pick su una fusione perché non si sa quale lato della fusione deve essere considerato il mainline. Questa opzione consente di riprodurre la modifica relativa al genitore specificato. + Di solito non è possibile fare cherry-pick sdi una unione perché non si sa quale lato deve essere considerato il mainline. Questa opzione consente di riprodurre la modifica relativa al genitore specificato. Cancella Stash Stai per cancellare tutti gli stash. Sei sicuro di voler continuare? Clona Repository Remoto @@ -110,22 +111,26 @@ Copia Info Copia SHA Azione Personalizzata - Rebase Interattivo ${0}$ fino a Qui + Riallinea Interattivamente ${0}$ fino a Qui + Unisci a ${0}$ + Unisci ... Riallinea ${0}$ fino a Qui Ripristina ${0}$ fino a Qui Annulla Commit Modifica Salva come Patch... - Unisci al Genitore - Unisci Commit Figli fino a Qui + Compatta nel Genitore + Compatta Commit Figli fino a Qui MODIFICHE Cerca Modifiche... FILE File LFS + Cerca File... Sottomodulo INFORMAZIONI AUTORE MODIFICATO + FIGLI CHI HA COMMITTATO Controlla i riferimenti che contengono questo commit IL COMMIT È CONTENUTO DA @@ -155,21 +160,21 @@ Recupera automaticamente i remoti Minuto/i Remoto Predefinito - Abilita --prune durante il fetch - Abilita --signoff per i commit TRACCIAMENTO ISSUE - Aggiungi Regola Esempio per GitHub - Aggiungi Regola Esempio per Jira - Aggiungi Regola Esempio per Issue GitLab - Aggiungi Regola Esempio per Merge Request GitLab + Aggiungi una regola di esempio per un Issue Gitee + Aggiungi una regola di esempio per un Pull Request Gitee + Aggiungi una regola di esempio per GitHub + Aggiungi una regola di esempio per Jira + Aggiungi una regola di esempio per Issue GitLab + Aggiungi una regola di esempio per una Merge Request GitLab Nuova Regola Espressione Regex Issue: Nome Regola: URL Risultato: Utilizza $1, $2 per accedere ai valori dei gruppi regex. AI - Servizio Preferito: - Se il 'Servizio Preferito' è impostato, SourceGit utilizzerà solo quello per questo repository. In caso contrario, se sono disponibili più servizi, verrà mostrato un menu contestuale per sceglierne uno. + Servizio preferito: + Se il 'Servizio Preferito' é impostato, SourceGit utilizzerà solo quello per questo repository. Altrimenti, se ci sono più servizi disponibili, verrà mostrato un menu contestuale per sceglierne uno. Proxy HTTP Proxy HTTP usato da questo repository Nome Utente @@ -194,9 +199,10 @@ Modifiche Locali: Scarta Non Fare Nulla - Stash e Ripristina + Stasha e Ripristina Nome Nuovo Branch: Inserisci il nome del branch. + Gli spazi verranno rimpiazzati con dei trattini. Crea Branch Locale Crea Tag... Nuovo Tag Su: @@ -238,6 +244,7 @@ Differenza Successiva NESSUNA MODIFICA O SOLO CAMBIAMENTI DI FINE LINEA Differenza Precedente + Abilita la navigazione a blocchi Salva come Patch Mostra Simboli Nascosti Diff Affiancato @@ -268,6 +275,7 @@ Avanzamento Veloce (senza verifica) Recupera Recupera da tutti i remoti + Forza la sovrascrittura dei riferimenti locali Recupera senza tag Remoto: Recupera Modifiche Remote @@ -276,15 +284,16 @@ Scarta {0} file... Scarta Modifiche nelle Righe Selezionate Apri Strumento di Merge Esterno + Risolvi Usando ${0}$ Salva come Patch... - Staging - Staging {0} file - Staging Modifiche nelle Righe Selezionate - Stash... - Stash {0} file... - Rimuovi dallo Staging - Rimuovi dallo Staging {0} file - Rimuovi dallo Staging Modifiche nelle Righe Selezionate + Stage + Stage di {0} file + Stage delle Modifiche nelle Righe Selezionate + Stasha... + Stasha {0} file... + Rimuovi da Stage + Rimuovi da Stage {0} file + Rimuovi le Righe Selezionate da Stage Usa Il Loro (checkout --theirs) Usa Il Mio (checkout --ours) Cronologia File @@ -297,22 +306,22 @@ Prefisso Feature: FLOW - Completa Feature FLOW - Completa Hotfix - FLOW - Completa Release + FLOW - Completa Rilascio Target: Hotfix: Prefisso Hotfix: Inizializza Git-Flow Mantieni branch Branch di Produzione: - Release: - Prefisso Release: + Rilascio: + Prefisso Rilascio: Inizia Feature... FLOW - Inizia Feature Inizia Hotfix... FLOW - Inizia Hotfix Inserisci nome - Inizia Release... - FLOW - Inizia Release + Inizia Rilascio... + FLOW - Inizia Rilascio Prefisso Tag Versione: Git LFS Aggiungi Modello di Tracciamento... @@ -323,28 +332,28 @@ Recupera Oggetti LFS Esegui `git lfs fetch` per scaricare gli oggetti Git LFS. Questo non aggiorna la copia di lavoro. Installa hook di Git LFS - Mostra Bloccaggi + Mostra Blocchi Nessun File Bloccato Blocca - Mostra solo i miei bloccaggi - Bloccaggi LFS + Mostra solo i miei blocchi + Blocchi LFS Sblocca Forza Sblocco Elimina Esegui `git lfs prune` per eliminare vecchi file LFS dallo storage locale - Pull - Pull Oggetti LFS + Scarica + Scarica Oggetti LFS Esegui `git lfs pull` per scaricare tutti i file LFS per il ref corrente e fare il checkout - Push - Push Oggetti LFS + Invia + Invia Oggetti LFS Invia grandi file in coda al punto finale di Git LFS Remoto: Traccia file con nome '{0}' Traccia tutti i file *{0} - Storico + STORICO AUTORE ORA AUTORE - GRAFICO & OGGETTO + GRAFICO E OGGETTO SHA ORA COMMIT {0} COMMIT SELEZIONATI @@ -359,19 +368,19 @@ Vai alla pagina precedente Vai alla pagina successiva Crea una nuova pagina - Apri la finestra di preferenze + Apri la finestra delle preferenze REPOSITORY - Conferma le modifiche in fase - Conferma e invia le modifiche in fase - Aggiungi tutte le modifiche e conferma + Committa le modifiche in tsage + Committa e invia le modifiche in stage + Fai lo stage di tutte le modifiche e committa Crea un nuovo branch dal commit selezionato Scarta le modifiche selezionate Recupera, avvia direttamente Modalità Dashboard (Predefinita) - Recupera e integra, avvia direttamente + Scarica, avvia direttamente Invia, avvia direttamente - Forza il ricaricamento di questo repository - Aggiungi/Rimuovi le modifiche selezionate + Forza l'aggiornamento di questo repository + Aggiungi/Rimuovi da stage le modifiche selezionate Modalità ricerca commit Passa a 'Modifiche' Passa a 'Storico' @@ -381,16 +390,20 @@ Trova il prossimo risultato Trova il risultato precedente Apri il pannello di ricerca - Aggiungi + Aggiungi in stage Rimuovi Scarta Inizializza Repository Percorso: Cherry-Pick in corso. - Richiesta di merge in corso. - Rebase in corso. - Revert in corso. - Rebase Interattivo + Elaborando il commit + Unione in corso. + Unendo + Riallineamento in corso. + Interrotto a + Ripristino in corso. + Ripristinando il commit + Riallinea Interattivamente Branch di destinazione: Su: Apri nel Browser @@ -399,11 +412,16 @@ AVVISO Unisci Branch In: - Opzione di Merge: + Opzione di Unione: + Sorgente: + Unione (multipla) + Commit di tutte le modifiche + Strategia: + Obiettivi: Sposta Nodo Repository Seleziona nodo padre per: Nome: - Git NON è configurato. Vai su [Preferenze] e configurarlo prima. + Git NON è configurato. Prima vai su [Preferenze] per configurarlo. Apri Cartella Dati App Apri con... Opzionale. @@ -428,7 +446,7 @@ AI Analizza il Prompt Differenza Chiave API - Genera Prompt Soggetto + Genera Prompt Oggetto Modello Nome Server @@ -449,20 +467,24 @@ Strumento GENERALE Controlla aggiornamenti all'avvio + Formato data Lingua Numero massimo di commit nella cronologia - Mostra l'orario dell'autore anziché quello del commit nel grafico - Lunghezza Guida Soggetto + Mostra nel grafico l'orario dell'autore anziché quello del commit + Mostra i figli nei dettagli del commit + Lunghezza Guida Oggetto GIT Abilita Auto CRLF Cartella predefinita per cloni Email Utente - Email globale utente Git + Email utente Git globale + Abilita --prune durante il fetch Percorso Installazione Nome Utente - Nome globale utente Git + Nome utente Git globale Versione di Git - Git (>= 2.23.0) è richiesto da questa applicazione + Questa applicazione richiede Git (>= 2.23.0) + Abilita la verifica HTTP SSL FIRMA GPG Firma GPG per commit Firma GPG per tag @@ -479,21 +501,21 @@ Destinazione: Potatura Worktrees Potatura delle informazioni di worktree in `$GIT_DIR/worktrees` - Pull + Scarica Branch Remoto: Recupera tutti i branch In: Modifiche Locali: Scarta Non fare nulla - Accantona e Riapplica + Stasha e Riapplica Recupera senza tag Remoto: - Pull (Fetch & Merge) - Usa rebase anziché merge - Push - Assicurati che i submoduli siano stati spinti - Forza il push + Scarica (Recupera e Unisci) + Riallineare anziché unire + Invia + Assicurati che i sottomoduli siano stati inviati + Forza l'invio Branch Locale: Remoto: Invia modifiche al remoto @@ -505,10 +527,10 @@ Remoto: Tag: Esci - Rebase Branch Corrente - Accantona & Riapplica modifiche locali + Riallinea Branch Corrente + Stasha e Riapplica modifiche locali Su: - Rebase: + Riallinea: Aggiorna Aggiungi Remoto Modifica Remoto @@ -531,7 +553,7 @@ Branch: ANNULLA Recupero automatico delle modifiche dai remoti... - Pulizia (GC & Potatura) + Pulizia (GC e Potatura) Esegui il comando `git gc` per questo repository. Cancella tutto Configura questo repository @@ -539,32 +561,50 @@ Azioni Personalizzate Nessuna Azione Personalizzata Abilita opzione '--reflog' - Apri nel Browser File - Cerca Branch/Tag/Submodule + Apri nell'Esplora File + Cerca Branch/Tag/Sottomodulo FILTRATO DA: + Visibilità nel grafico + Non impostato + Nascondi nel grafico dei commit + Filtra nel grafico dei commit + LAYOUT + Orizzontale + Verticale + Ordine dei commit + Per data del commit + Topologicamente BRANCH LOCALI Vai a HEAD Abilita opzione '--first-parent' Crea Branch + Evidenzia nel grafico solo il branch corrente Apri in {0} Apri in Strumenti Esterni Aggiorna REMOTI AGGIUNGI REMOTO Cerca Commit + Autore + Committente File Messaggio SHA - Autore & Committente Branch Corrente Mostra Tag come Albero + SALTA Statistiche - SUBMODULE - AGGIUNGI SUBMODULE - AGGIORNA SUBMODULE + SOTTOMODULI + AGGIUNGI SOTTOMODULI + AGGIORNA SOTTOMODULI TAG NUOVO TAG + Per data di creazione + Per nome (ascendente) + Per nome (discendente) + Ordina Apri nel Terminale + Usa tempo relativo nello storico WORKTREE AGGIUNGI WORKTREE POTATURA @@ -573,10 +613,10 @@ Modalità Reset: Sposta a: Branch Corrente: - Mostra nel File Explorer - Revert Commit + Mostra nell'Esplora File + Ripristina Commit Commit: - Commit delle modifiche di revert + Commit delle modifiche di ripristino Modifica Messaggio di Commit Usa 'Shift+Enter' per inserire una nuova riga. 'Enter' è il tasto rapido per il pulsante OK In esecuzione. Attendere... @@ -592,27 +632,33 @@ Salta questa versione Aggiornamento Software Non ci sono aggiornamenti disponibili. - Squash Commit + Imposta il Branch + Branch: + Rimuovi upstream + Upstream: + Copia SHA + Vai a + Compatta Commit In: Chiave Privata SSH: Percorso per la chiave SSH privata AVVIA - Accantona + Stasha Includi file non tracciati - Mantieni file indicizzati + Mantieni file in stage Messaggio: - Opzionale. Nome di questo accantonamento - Solo modifiche indicizzate - Sia le modifiche indicizzate che quelle non indicizzate dei file selezionati saranno accantonate!!! - Accantona Modifiche Locali + Opzionale. Nome di questo stash + Solo modifiche in stage + Sia le modifiche in stage che quelle non in stage dei file selezionati saranno stashate!!! + Stasha Modifiche Locali Applica Elimina Estrai - Elimina Accantonamento + Elimina Stash Elimina: - Accantonamenti + STASH MODIFICHE - ACCANTONAMENTI + STASH Statistiche COMMIT COMMITTER @@ -621,14 +667,14 @@ COMMIT: AUTORI: PANORAMICA - SUBMODULE - Aggiungi Submodule + SOTTOMODULI + Aggiungi Sottomodulo Copia Percorso Relativo - Recupera submodule annidati - Apri Repository Submodule + Recupera sottomoduli annidati + Apri Repository del Sottomodulo Percorso Relativo: Cartella relativa per memorizzare questo modulo. - Elimina Submodule + Elimina Sottomodulo OK Copia Nome Tag Copia Messaggio Tag @@ -636,11 +682,11 @@ Unisci ${0}$ in ${1}$... Invia ${0}$... URL: - Aggiorna Submodule - Tutti i submodule + Aggiorna Sottomoduli + Tutti i sottomoduli Inizializza se necessario Ricorsivamente - Submodule: + Sottomodulo: Usa opzione --remote Avviso Pagina di Benvenuto @@ -648,7 +694,7 @@ Crea Sottogruppo Clona Repository Elimina - TRASCINA & RILASCIA CARTELLA SUPPORTATO. RAGGRUPPAMENTI PERSONALIZZATI SUPPORTATI. + TRASCINA E RILASCIA CARTELLA SUPPORTATO. RAGGRUPPAMENTI PERSONALIZZATI SUPPORTATI. Modifica Sposta in un Altro Gruppo Apri Tutti i Repository @@ -657,34 +703,36 @@ Riscansiona Repository nella Cartella Clone Predefinita Cerca Repository... Ordina - Modifiche + MODIFICHE LOCALI Git Ignore Ignora tutti i file *{0} Ignora i file *{0} nella stessa cartella Ignora i file nella stessa cartella Ignora solo questo file Modifica - Puoi indicizzare questo file ora. + Puoi aggiungere in stage questo file ora. COMMIT - COMMIT & PUSH + COMMIT E INVIA Template/Storico Attiva evento click - Indica tutte le modifiche e fai il commit - Commit vuoto rilevato! Vuoi continuare (--allow-empty)? + Commit (Modifica) + Stage di tutte le modifiche e fai il commit + Trovato un commit vuoto! Vuoi continuare (--allow-empty)? CONFLITTI RILEVATI CONFLITTI NEI FILE RISOLTI INCLUDI FILE NON TRACCIATI NESSUN MESSAGGIO RECENTE INSERITO NESSUN TEMPLATE DI COMMIT - INDICIZZATI - RIMUOVI DALL'INDICIZZAZIONE - RIMUOVI TUTTO DALL'INDICIZZAZIONE - NON INDICIZZATI - INDICIZZA - INDICIZZA TUTTO + IN STAGE + RIMUOVI DA STAGE + RIMUOVI TUTTO DA STAGE + NON IN STAGE + FAI LO STAGE + FAI LO STAGE DI TUTTO VISUALIZZA COME NON MODIFICATO Template: ${0}$ - Clicca con il tasto destro sul file(i) selezionato, quindi scegli come risolvere i conflitti. + Clicca con il tasto destro sul(i) file selezionato, quindi scegli come risolvere i conflitti. + SignOff WORKSPACE: Configura Workspaces... WORKTREE diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index 8b17bdaf..dee8565b 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -180,8 +180,6 @@ Buscar remotos automaticamente Minuto(s) Remoto padrão - Habilita --prune ao buscar - Habilita --signoff para commits RASTREADOR DE PROBLEMAS Adicionar Regra de Exemplo do Github Adicionar Regra de Exemplo do Jira @@ -481,6 +479,7 @@ Diretório de Clone Padrão Email do Usuário Email global do usuário git + Habilita --prune ao buscar Caminho de Instalação Nome do Usuário Nome global do usuário git @@ -567,8 +566,8 @@ Desfazer Esconder no gráfico de commit Incluir no gráfico de commit - Data do Commit (--date-order) - Topologicamente (--topo-order) + Data do Commit + Topologicamente BRANCHES LOCAIS Navegar para HEAD Habilitar opção '--first-parent' @@ -579,10 +578,11 @@ REMOTOS ADICIONAR REMOTO Pesquisar Commit + Autor + Committer Arquivo Mensagem SHA - Autor & Committer Branch Atual Exibir Tags como Árvore Estatísticas diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 75650a28..27a2d360 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -159,9 +159,7 @@ GIT Автоматическое скачивание изменений Минут(а/ы) - Разрешить '--signoff' для ревизии Внешний репозиторий по умолчанию - Разрешить '--prune' при скачивании ОТСЛЕЖИВАНИЕ ПРОБЛЕМ Добавить пример правила для тем в Gitee Добавить пример правила запроса скачивания из Gitee @@ -479,6 +477,7 @@ Каталог клонирования по умолчанию Электроная почта пользователя Общая электроная почта пользователя git + Разрешить '--prune' при скачивании Путь установки Разрешить верификацию HTTP SSL Имя пользователя @@ -572,8 +571,8 @@ Горизонтально Вертикально ЗАПРОС РЕВИЗИЙ - Дата ревизии (--date-order) - Топологически (--topo-order) + Дата ревизии + Топологически ЛОКАЛЬНЫЕ ВЕТКИ Навигация по ГОЛОВЕ (HEAD) Включить опцию --first-parent @@ -585,10 +584,11 @@ ВНЕШНИЕ РЕПОЗИТОРИИ ДОБАВИТЬ ВНЕШНИЙ РЕПОЗИТОРИЙ Поиск ревизии + Автор + исполнитель Файл Сообщение SHA - Автор и исполнитель Текущая ветка Показывать метки как катлог ПРОПУСТИТЬ diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index b5541376..66d1f37e 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -22,7 +22,9 @@ 跟踪分支 设置上游跟踪分支 AI助手 + 重新生成 使用AI助手生成提交信息 + 应用本次生成 应用补丁(apply) 错误 输出错误,并终止应用补丁 @@ -37,6 +39,10 @@ 警告 应用补丁,输出关于空白符的警告 空白符号处理 : + 应用贮藏 + 在成功应用后丢弃该贮藏 + 恢复索引中已暂存的变化 + 已选贮藏 : 存档(archive) ... 存档文件路径: 选择存档文件的存放路径 @@ -100,6 +106,7 @@ 本地仓库名 : 本地仓库目录的名字,选填。 父级目录 : + 初始化并更新子模块 远程仓库 : 关闭 提交信息编辑器 @@ -160,8 +167,6 @@ 启用定时自动拉取远程更新 分钟 默认远程 - 提交信息追加署名 (--signoff) - 拉取更新时启用修剪(--prune) ISSUE追踪 新增匹配Gitee议题规则 新增匹配Gitee合并请求规则 @@ -204,6 +209,7 @@ 贮藏并自动恢复 新分支名 : 填写分支名称。 + 空格将被替换为'-'符号 创建本地分支 新建标签 ... 标签位于 : @@ -227,8 +233,11 @@ 您正在尝试一次性删除多个分支,请务必仔细检查后再执行操作! 删除远程确认 远程名 : + 路径 : 目标 : + 所有子节点将被同时从列表中移除。 删除分组确认 + 仅从列表中移除,不会删除硬盘中的文件! 删除仓库确认 删除子模块确认 子模块路径 : @@ -276,7 +285,7 @@ 快进(fast-forward,无需checkout) 拉取(fetch) 拉取所有的远程仓库 - 覆盖REF检查 + 强制覆盖本地REFs 不拉取远程标签 远程仓库 : 拉取远程仓库内容 @@ -479,6 +488,7 @@ 默认克隆路径 邮箱 默认GIT用户邮箱 + 拉取更新时启用修剪(--prune) 安装路径 启用HTTP SSL验证 用户名 @@ -571,8 +581,8 @@ 水平排布 竖直排布 提交列表排序规则 - 按提交时间 (--date-order) - 按拓扑排序 (--topo-order) + 按提交时间 + 按拓扑排序 本地分支 定位HEAD 启用 --first-parent 过滤选项 @@ -584,10 +594,11 @@ 远程列表 添加远程 查找提交 + 作者 + 提交者 文件 提交信息 提交指纹 - 作者及提交者 仅在当前分支中查找 以树型结构展示 跳过此提交 @@ -642,6 +653,8 @@ SSH密钥文件 开 始 贮藏(stash) + 贮藏后自动恢复工作区 + 工作区文件保持未修改状态,但贮藏内容已保存。 包含未跟踪的文件 保留暂存区文件 信息 : @@ -707,7 +720,7 @@ 忽略同目录下所有 *{0} 文件 忽略同目录下所有文件 忽略本文件 - 修补(--amend) + 修补 现在您已可将其加入暂存区中 提交 提交并推送 @@ -721,6 +734,7 @@ 显示未跟踪文件 没有提交信息记录 没有可应用的提交信息模板 + 署名 已暂存 从暂存区移除选中 从暂存区移除所有 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index d2395e58..f8255947 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -22,7 +22,9 @@ 追蹤分支 設定遠端追蹤分支 AI 助理 + 重新產生 使用 AI 產生提交訊息 + 套用為提交訊息 套用修補檔 (apply patch) 錯誤 輸出錯誤,並中止套用修補檔 @@ -37,6 +39,10 @@ 警告 套用修補檔,輸出關於空白字元的警告 空白字元處理: + 套用擱置 + 在成功套用后捨棄擱置 + 恢復索引中已暫存的變更 + 已選擇擱置 : 封存 (archive)... 封存檔案路徑: 選擇封存檔案的儲存路徑 @@ -100,6 +106,7 @@ 本機存放庫名稱: 本機存放庫目錄的名稱,選填。 父級目錄: + 初始化並複製子模組 遠端存放庫: 關閉 提交訊息編輯器 @@ -160,8 +167,6 @@ 啟用定時自動提取 (fetch) 遠端更新 分鐘 預設遠端存放庫 - 提交訊息追加署名 (--signoff) - 拉取變更時進行清理 (--prune) Issue 追蹤 新增符合 Gitee 議題規則 新增符合 Gitee 合併請求規則 @@ -204,6 +209,7 @@ 擱置變更並自動復原 新分支名稱: 輸入分支名稱。 + 空格將以英文破折號取代 建立本機分支 新增標籤... 標籤位於: @@ -227,8 +233,11 @@ 您正在嘗試一次性刪除多個分支,請務必仔細檢查後再刪除! 刪除遠端確認 遠端名稱: + 路徑: 目標: + 所有子節點都會從清單中移除。 刪除群組確認 + 只會從清單中移除,而不會刪除磁碟中的檔案! 刪除存放庫確認 刪除子模組確認 子模組路徑: @@ -276,7 +285,7 @@ 快進 (fast-forward,無需 checkout) 提取 (fetch) 提取所有的遠端存放庫 - 覆寫 REFs 檢查 + 強制覆寫本機 REFs 不提取遠端標籤 遠端存放庫: 提取遠端存放庫內容 @@ -478,6 +487,7 @@ 預設複製 (clone) 路徑 電子郵件 預設 Git 使用者電子郵件 + 拉取變更時進行清理 (--prune) 安裝路徑 啟用 HTTP SSL 驗證 使用者名稱 @@ -570,8 +580,8 @@ 橫向顯示 縱向顯示 提交顯示順序 - 依提交時間排序 (--date-order) - 依拓撲排序 (--topo-order) + 依提交時間排序 + 依拓撲排序 本機分支 回到 HEAD 啟用 [--first-parent] 選項 @@ -583,10 +593,11 @@ 遠端列表 新增遠端 搜尋提交 + 作者 + 提交者 檔案 提交訊息 提交編號 - 作者及提交者 僅搜尋目前分支 以樹型結構展示 跳過此提交 @@ -641,6 +652,8 @@ SSH 金鑰檔案 開 始 擱置變更 (stash) + 暫存後自動復原工作區 + 工作區檔案保持未修改,但暫存內容已儲存。 包含未追蹤的檔案 保留已暫存的變更 擱置變更訊息: @@ -706,7 +719,7 @@ 忽略同路徑下所有 *{0} 檔案 忽略同路徑下所有檔案 忽略本檔案 - 修補 (--amend) + 修補 現在您已可將其加入暫存區中 提 交 提交並推送 @@ -720,6 +733,7 @@ 顯示未追蹤檔案 沒有提交訊息記錄 沒有可套用的提交訊息範本 + 署名 已暫存 取消暫存選取的檔案 取消暫存所有檔案 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 6335c635..9b43d8af 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="using:SourceGit" xmlns:vm="using:SourceGit.ViewModels" + xmlns:v="using:SourceGit.Views" xmlns:c="using:SourceGit.Converters" xmlns:ae="using:AvaloniaEdit" xmlns:aee="using:AvaloniaEdit.Editing" @@ -844,7 +845,19 @@ HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="{DynamicResource MenuFlyoutItemKeyboardAcceleratorTextForeground}" - FontSize="11"/> + FontSize="11" + IsVisible="{TemplateBinding (v:MenuItemExtension.Command), Converter={x:Static StringConverters.IsNullOrEmpty}}"/> + + + - + + diff --git a/src/ViewModels/AddRemote.cs b/src/ViewModels/AddRemote.cs index 2ca7449f..d6424572 100644 --- a/src/ViewModels/AddRemote.cs +++ b/src/ViewModels/AddRemote.cs @@ -100,7 +100,7 @@ namespace SourceGit.ViewModels { SetProgressDescription("Fetching from added remote ..."); new Commands.Config(_repo.FullPath).Set($"remote.{_name}.sshkey", _useSSH ? SSHKey : null); - new Commands.Fetch(_repo.FullPath, _name, false, false, false, SetProgressDescription).Exec(); + new Commands.Fetch(_repo.FullPath, _name, false, false, SetProgressDescription).Exec(); } CallUIThread(() => { diff --git a/src/ViewModels/AddWorktree.cs b/src/ViewModels/AddWorktree.cs index cf736029..6c1c7481 100644 --- a/src/ViewModels/AddWorktree.cs +++ b/src/ViewModels/AddWorktree.cs @@ -12,7 +12,7 @@ namespace SourceGit.ViewModels public string Path { get => _path; - set => SetProperty(ref _path, value); + set => SetProperty(ref _path, value, true); } public bool CreateNewBranch diff --git a/src/ViewModels/ApplyStash.cs b/src/ViewModels/ApplyStash.cs new file mode 100644 index 00000000..03ce0f43 --- /dev/null +++ b/src/ViewModels/ApplyStash.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class ApplyStash : Popup + { + public Models.Stash Stash + { + get; + private set; + } + + public bool RestoreIndex + { + get; + set; + } = true; + + public bool DropAfterApply + { + get; + set; + } = false; + + public ApplyStash(string repo, Models.Stash stash) + { + _repo = repo; + Stash = stash; + View = new Views.ApplyStash() { DataContext = this }; + } + + public override Task Sure() + { + ProgressDescription = $"Applying stash: {Stash.Name}"; + + return Task.Run(() => + { + var succ = new Commands.Stash(_repo).Apply(Stash.Name, RestoreIndex); + if (succ && DropAfterApply) + new Commands.Stash(_repo).Drop(Stash.Name); + + return true; + }); + } + + private readonly string _repo; + } +} diff --git a/src/ViewModels/Clone.cs b/src/ViewModels/Clone.cs index 98d3bf41..3123d62a 100644 --- a/src/ViewModels/Clone.cs +++ b/src/ViewModels/Clone.cs @@ -53,6 +53,12 @@ namespace SourceGit.ViewModels set => SetProperty(ref _extraArgs, value); } + public bool InitAndUpdateSubmodules + { + get; + set; + } = true; + public Clone(string pageId) { _pageId = pageId; @@ -127,6 +133,17 @@ namespace SourceGit.ViewModels config.Set("remote.origin.sshkey", _sshKey); } + // individually update submodule (if any) + if (InitAndUpdateSubmodules) + { + var submoduleList = new Commands.QuerySubmodules(path).Result(); + foreach (var submodule in submoduleList) + { + var update = new Commands.Submodule(path); + update.Update(submodule.Path, true, true, false, SetProgressDescription); + } + } + CallUIThread(() => { var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(path, null, true); diff --git a/src/ViewModels/CreateBranch.cs b/src/ViewModels/CreateBranch.cs index b67a453a..a9698a07 100644 --- a/src/ViewModels/CreateBranch.cs +++ b/src/ViewModels/CreateBranch.cs @@ -6,7 +6,7 @@ namespace SourceGit.ViewModels public class CreateBranch : Popup { [Required(ErrorMessage = "Branch name is required!")] - [RegularExpression(@"^[\w\-/\.#]+$", ErrorMessage = "Bad branch name format!")] + [RegularExpression(@"^[\w \-/\.#]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(CreateBranch), nameof(ValidateBranchName))] public string Name { @@ -74,9 +74,10 @@ namespace SourceGit.ViewModels if (creator == null) return new ValidationResult("Missing runtime context to create branch!"); + var fixedName = creator.FixName(name); foreach (var b in creator._repo.Branches) { - if (b.FriendlyName == name) + if (b.FriendlyName == fixedName) return new ValidationResult("A branch with same name already exists!"); } @@ -86,6 +87,8 @@ namespace SourceGit.ViewModels public override Task Sure() { _repo.SetWatcherEnabled(false); + + var fixedName = FixName(_name); return Task.Run(() => { var succ = false; @@ -114,8 +117,8 @@ namespace SourceGit.ViewModels } } - SetProgressDescription($"Create new branch '{_name}'"); - succ = new Commands.Checkout(_repo.FullPath).Branch(_name, _baseOnRevision, SetProgressDescription); + SetProgressDescription($"Create new branch '{fixedName}'"); + succ = new Commands.Checkout(_repo.FullPath).Branch(fixedName, _baseOnRevision, SetProgressDescription); if (needPopStash) { @@ -125,15 +128,15 @@ namespace SourceGit.ViewModels } else { - SetProgressDescription($"Create new branch '{_name}'"); - succ = Commands.Branch.Create(_repo.FullPath, _name, _baseOnRevision); + SetProgressDescription($"Create new branch '{fixedName}'"); + succ = Commands.Branch.Create(_repo.FullPath, fixedName, _baseOnRevision); } CallUIThread(() => { if (succ && CheckoutAfterCreated) { - var fake = new Models.Branch() { IsLocal = true, FullName = $"refs/heads/{_name}" }; + var fake = new Models.Branch() { IsLocal = true, FullName = $"refs/heads/{fixedName}" }; if (BasedOn is Models.Branch based && !based.IsLocal) fake.Upstream = based.FullName; @@ -153,6 +156,15 @@ namespace SourceGit.ViewModels }); } + private string FixName(string name) + { + if (!name.Contains(' ')) + return name; + + var parts = name.Split(' ', System.StringSplitOptions.RemoveEmptyEntries); + return string.Join("-", parts); + } + private readonly Repository _repo = null; private string _name = null; private readonly string _baseOnRevision = null; diff --git a/src/ViewModels/Fetch.cs b/src/ViewModels/Fetch.cs index 2d907edd..d816d0b8 100644 --- a/src/ViewModels/Fetch.cs +++ b/src/ViewModels/Fetch.cs @@ -47,7 +47,6 @@ namespace SourceGit.ViewModels _repo.SetWatcherEnabled(false); var notags = _repo.Settings.FetchWithoutTags; - var prune = _repo.Settings.EnablePruneOnFetch; var force = _repo.Settings.EnableForceOnFetch; return Task.Run(() => { @@ -56,13 +55,13 @@ namespace SourceGit.ViewModels foreach (var remote in _repo.Remotes) { SetProgressDescription($"Fetching remote: {remote.Name}"); - new Commands.Fetch(_repo.FullPath, remote.Name, notags, prune, force, SetProgressDescription).Exec(); + new Commands.Fetch(_repo.FullPath, remote.Name, notags, force, SetProgressDescription).Exec(); } } else { SetProgressDescription($"Fetching remote: {SelectedRemote.Name}"); - new Commands.Fetch(_repo.FullPath, SelectedRemote.Name, notags, prune, force, SetProgressDescription).Exec(); + new Commands.Fetch(_repo.FullPath, SelectedRemote.Name, notags, force, SetProgressDescription).Exec(); } CallUIThread(() => diff --git a/src/ViewModels/FileHistories.cs b/src/ViewModels/FileHistories.cs index ede73cd1..7e248274 100644 --- a/src/ViewModels/FileHistories.cs +++ b/src/ViewModels/FileHistories.cs @@ -4,6 +4,7 @@ using System.IO; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Avalonia.Collections; using Avalonia.Media.Imaging; using Avalonia.Threading; @@ -17,36 +18,14 @@ namespace SourceGit.ViewModels public object Content { get; set; } = content; } - public partial class FileHistories : ObservableObject + public partial class FileHistoriesSingleRevision : ObservableObject { - public bool IsLoading + public bool IsDiffMode { - get => _isLoading; - private set => SetProperty(ref _isLoading, value); - } - - public List Commits - { - get => _commits; - set => SetProperty(ref _commits, value); - } - - public Models.Commit SelectedCommit - { - get => _selectedCommit; + get => _isDiffMode; set { - if (SetProperty(ref _selectedCommit, value)) - RefreshViewContent(); - } - } - - public bool IsViewContent - { - get => _isViewContent; - set - { - if (SetProperty(ref _isViewContent, value)) + if (SetProperty(ref _isDiffMode, value)) RefreshViewContent(); } } @@ -54,55 +33,36 @@ namespace SourceGit.ViewModels public object ViewContent { get => _viewContent; - private set => SetProperty(ref _viewContent, value); + set => SetProperty(ref _viewContent, value); } - public FileHistories(Repository repo, string file, string commit = null) + public FileHistoriesSingleRevision(Repository repo, string file, Models.Commit revision, bool prevIsDiffMode) { _repo = repo; _file = file; + _revision = revision; + _isDiffMode = prevIsDiffMode; + _viewContent = null; - Task.Run(() => - { - var based = commit ?? string.Empty; - var commits = new Commands.QueryCommits(_repo.FullPath, $"--date-order -n 10000 {based} -- \"{file}\"", false).Result(); - Dispatcher.UIThread.Invoke(() => - { - IsLoading = false; - Commits = commits; - if (commits.Count > 0) - SelectedCommit = commits[0]; - }); - }); - } - - public void NavigateToCommit(Models.Commit commit) - { - _repo.NavigateToCommit(commit.SHA); + RefreshViewContent(); } public void ResetToSelectedRevision() { - new Commands.Checkout(_repo.FullPath).FileWithRevision(_file, $"{_selectedCommit.SHA}"); + new Commands.Checkout(_repo.FullPath).FileWithRevision(_file, $"{_revision.SHA}"); } private void RefreshViewContent() { - if (_selectedCommit == null) - { - ViewContent = null; - return; - } - - if (_isViewContent) - SetViewContentAsRevisionFile(); - else + if (_isDiffMode) SetViewContentAsDiff(); + else + SetViewContentAsRevisionFile(); } private void SetViewContentAsRevisionFile() { - var objs = new Commands.QueryRevisionObjects(_repo.FullPath, _selectedCommit.SHA, _file).Result(); + var objs = new Commands.QueryRevisionObjects(_repo.FullPath, _revision.SHA, _file).Result(); if (objs.Count == 0) { ViewContent = new FileHistoriesRevisionFile(_file, null); @@ -115,13 +75,13 @@ namespace SourceGit.ViewModels case Models.ObjectType.Blob: Task.Run(() => { - var isBinary = new Commands.IsBinary(_repo.FullPath, _selectedCommit.SHA, _file).Result(); + var isBinary = new Commands.IsBinary(_repo.FullPath, _revision.SHA, _file).Result(); if (isBinary) { var ext = Path.GetExtension(_file); if (IMG_EXTS.Contains(ext)) { - var stream = Commands.QueryFileContent.Run(_repo.FullPath, _selectedCommit.SHA, _file); + var stream = Commands.QueryFileContent.Run(_repo.FullPath, _revision.SHA, _file); var fileSize = stream.Length; var bitmap = fileSize > 0 ? new Bitmap(stream) : null; var imageType = Path.GetExtension(_file).TrimStart('.').ToUpper(CultureInfo.CurrentCulture); @@ -130,7 +90,7 @@ namespace SourceGit.ViewModels } else { - var size = new Commands.QueryFileSize(_repo.FullPath, _file, _selectedCommit.SHA).Result(); + var size = new Commands.QueryFileSize(_repo.FullPath, _file, _revision.SHA).Result(); var binaryFile = new Models.RevisionBinaryFile() { Size = size }; Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, binaryFile)); } @@ -138,7 +98,7 @@ namespace SourceGit.ViewModels return; } - var contentStream = Commands.QueryFileContent.Run(_repo.FullPath, _selectedCommit.SHA, _file); + var contentStream = Commands.QueryFileContent.Run(_repo.FullPath, _revision.SHA, _file); var content = new StreamReader(contentStream).ReadToEnd(); var matchLFS = REG_LFS_FORMAT().Match(content); if (matchLFS.Success) @@ -181,7 +141,7 @@ namespace SourceGit.ViewModels private void SetViewContentAsDiff() { - var option = new Models.DiffOption(_selectedCommit, _file); + var option = new Models.DiffOption(_revision, _file); ViewContent = new DiffContext(_repo.FullPath, option, _viewContent as DiffContext); } @@ -193,12 +153,155 @@ namespace SourceGit.ViewModels ".ico", ".bmp", ".jpg", ".png", ".jpeg", ".webp" }; + private Repository _repo = null; + private string _file = null; + private Models.Commit _revision = null; + private bool _isDiffMode = true; + private object _viewContent = null; + } + + public class FileHistoriesCompareRevisions : ObservableObject + { + public Models.Commit StartPoint + { + get => _startPoint; + set => SetProperty(ref _startPoint, value); + } + + public Models.Commit EndPoint + { + get => _endPoint; + set => SetProperty(ref _endPoint, value); + } + + public DiffContext ViewContent + { + get => _viewContent; + set => SetProperty(ref _viewContent, value); + } + + public FileHistoriesCompareRevisions(Repository repo, string file, Models.Commit start, Models.Commit end) + { + _repo = repo; + _file = file; + _startPoint = start; + _endPoint = end; + RefreshViewContent(); + } + + public void Swap() + { + (StartPoint, EndPoint) = (_endPoint, _startPoint); + RefreshViewContent(); + } + + public Task SaveAsPatch(string saveTo) + { + return Task.Run(() => + { + Commands.SaveChangesAsPatch.ProcessRevisionCompareChanges(_repo.FullPath, _changes, _startPoint.SHA, _endPoint.SHA, saveTo); + return true; + }); + } + + private void RefreshViewContent() + { + Task.Run(() => + { + _changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, _file).Result(); + if (_changes.Count == 0) + { + Dispatcher.UIThread.Invoke(() => ViewContent = null); + return; + } + + var option = new Models.DiffOption(_startPoint.SHA, _endPoint.SHA, _changes[0]); + Dispatcher.UIThread.Invoke(() => ViewContent = new DiffContext(_repo.FullPath, option, _viewContent)); + }); + } + + private Repository _repo = null; + private string _file = null; + private Models.Commit _startPoint = null; + private Models.Commit _endPoint = null; + private List _changes = []; + private DiffContext _viewContent = null; + } + + public class FileHistories : ObservableObject + { + public bool IsLoading + { + get => _isLoading; + private set => SetProperty(ref _isLoading, value); + } + + public List Commits + { + get => _commits; + set => SetProperty(ref _commits, value); + } + + public AvaloniaList SelectedCommits + { + get; + set; + } = []; + + public object ViewContent + { + get => _viewContent; + private set => SetProperty(ref _viewContent, value); + } + + public FileHistories(Repository repo, string file, string commit = null) + { + _repo = repo; + _file = file; + + Task.Run(() => + { + var based = commit ?? string.Empty; + var commits = new Commands.QueryCommits(_repo.FullPath, $"--date-order -n 10000 {based} -- \"{file}\"", false).Result(); + Dispatcher.UIThread.Invoke(() => + { + IsLoading = false; + Commits = commits; + if (Commits.Count > 0) + SelectedCommits.Add(Commits[0]); + }); + }); + + SelectedCommits.CollectionChanged += (_, _) => + { + if (_viewContent is FileHistoriesSingleRevision singleRevision) + _prevIsDiffMode = singleRevision.IsDiffMode; + + switch (SelectedCommits.Count) + { + case 1: + ViewContent = new FileHistoriesSingleRevision(_repo, _file, SelectedCommits[0], _prevIsDiffMode); + break; + case 2: + ViewContent = new FileHistoriesCompareRevisions(_repo, _file, SelectedCommits[0], SelectedCommits[1]); + break; + default: + ViewContent = SelectedCommits.Count; + break; + } + }; + } + + public void NavigateToCommit(Models.Commit commit) + { + _repo.NavigateToCommit(commit.SHA); + } + private readonly Repository _repo = null; private readonly string _file = null; private bool _isLoading = true; + private bool _prevIsDiffMode = true; private List _commits = null; - private Models.Commit _selectedCommit = null; - private bool _isViewContent = false; private object _viewContent = null; } } diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index b5754d33..03f216de 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -285,7 +285,7 @@ namespace SourceGit.ViewModels if (canCherryPick || canMerge) multipleMenu.Items.Add(new MenuItem() { Header = "-" }); - } + } var saveToPatchMultiple = new MenuItem(); saveToPatchMultiple.Icon = App.CreateMenuIcon("Icons.Diff"); @@ -589,7 +589,7 @@ namespace SourceGit.ViewModels menu.Items.Add(interactiveRebase); menu.Items.Add(new MenuItem() { Header = "-" }); } - } + } if (current.Head != commit.SHA) { @@ -937,7 +937,7 @@ namespace SourceGit.ViewModels submenu.Items.Add(finish); submenu.Items.Add(new MenuItem() { Header = "-" }); } - } + } var copy = new MenuItem(); copy.Header = App.Text("BranchCM.CopyName"); @@ -983,7 +983,7 @@ namespace SourceGit.ViewModels e.Handled = true; }; submenu.Items.Add(merge); - } + } var rename = new MenuItem(); rename.Header = new Views.NameHighlightedTextBlock("BranchCM.Rename", branch.Name); @@ -1025,7 +1025,7 @@ namespace SourceGit.ViewModels submenu.Items.Add(finish); submenu.Items.Add(new MenuItem() { Header = "-" }); } - } + } var copy = new MenuItem(); copy.Header = App.Text("BranchCM.CopyName"); @@ -1131,7 +1131,7 @@ namespace SourceGit.ViewModels e.Handled = true; }; submenu.Items.Add(merge); - } + } var delete = new MenuItem(); delete.Header = new Views.NameHighlightedTextBlock("TagCM.Delete", tag.Name); diff --git a/src/ViewModels/InProgressContexts.cs b/src/ViewModels/InProgressContexts.cs index 6099c2b9..2892b7cc 100644 --- a/src/ViewModels/InProgressContexts.cs +++ b/src/ViewModels/InProgressContexts.cs @@ -107,8 +107,12 @@ namespace SourceGit.ViewModels { _gitDir = repo.GitDir; - var stoppedSHA = File.ReadAllText(Path.Combine(repo.GitDir, "rebase-merge", "stopped-sha")).Trim(); - StoppedAt = new Commands.QuerySingleCommit(repo.FullPath, stoppedSHA).Result() ?? new Models.Commit() { SHA = stoppedSHA }; + var stoppedSHAPath = Path.Combine(repo.GitDir, "rebase-merge", "stopped-sha"); + if (File.Exists(stoppedSHAPath)) + { + var stoppedSHA = File.ReadAllText(stoppedSHAPath).Trim(); + StoppedAt = new Commands.QuerySingleCommit(repo.FullPath, stoppedSHA).Result() ?? new Models.Commit() { SHA = stoppedSHA }; + } var ontoSHA = File.ReadAllText(Path.Combine(repo.GitDir, "rebase-merge", "onto")).Trim(); Onto = new Commands.QuerySingleCommit(repo.FullPath, ontoSHA).Result() ?? new Models.Commit() { SHA = ontoSHA }; diff --git a/src/ViewModels/LauncherPage.cs b/src/ViewModels/LauncherPage.cs index 498c1865..b8138aca 100644 --- a/src/ViewModels/LauncherPage.cs +++ b/src/ViewModels/LauncherPage.cs @@ -59,7 +59,9 @@ namespace SourceGit.ViewModels public void StartPopup(Popup popup) { Popup = popup; - ProcessPopup(); + + if (popup.CanStartDirectly()) + ProcessPopup(); } public async void ProcessPopup() diff --git a/src/ViewModels/Merge.cs b/src/ViewModels/Merge.cs index d07ee9b7..174bb1e1 100644 --- a/src/ViewModels/Merge.cs +++ b/src/ViewModels/Merge.cs @@ -61,9 +61,9 @@ namespace SourceGit.ViewModels return Task.Run(() => { - var succ = new Commands.Merge(_repo.FullPath, _sourceName, SelectedMode.Arg, SetProgressDescription).Exec(); + new Commands.Merge(_repo.FullPath, _sourceName, SelectedMode.Arg, SetProgressDescription).Exec(); CallUIThread(() => _repo.SetWatcherEnabled(true)); - return succ; + return true; }); } diff --git a/src/ViewModels/Popup.cs b/src/ViewModels/Popup.cs index ff74df51..98a12ca2 100644 --- a/src/ViewModels/Popup.cs +++ b/src/ViewModels/Popup.cs @@ -37,6 +37,11 @@ namespace SourceGit.ViewModels return !HasErrors; } + public virtual bool CanStartDirectly() + { + return true; + } + public virtual Task Sure() { return null; diff --git a/src/ViewModels/Pull.cs b/src/ViewModels/Pull.cs index e7c62980..52f98d87 100644 --- a/src/ViewModels/Pull.cs +++ b/src/ViewModels/Pull.cs @@ -151,7 +151,6 @@ namespace SourceGit.ViewModels _repo.FullPath, _selectedRemote.Name, NoTags, - _repo.Settings.EnablePruneOnFetch, false, SetProgressDescription).Exec(); @@ -184,7 +183,6 @@ namespace SourceGit.ViewModels _selectedBranch.Name, UseRebase, NoTags, - _repo.Settings.EnablePruneOnFetch, SetProgressDescription).Exec(); } diff --git a/src/ViewModels/Push.cs b/src/ViewModels/Push.cs index 004ae7b6..1f18b38e 100644 --- a/src/ViewModels/Push.cs +++ b/src/ViewModels/Push.cs @@ -152,6 +152,11 @@ namespace SourceGit.ViewModels View = new Views.Push() { DataContext = this }; } + public override bool CanStartDirectly() + { + return !string.IsNullOrEmpty(_selectedRemoteBranch?.Head); + } + public override Task Sure() { _repo.SetWatcherEnabled(false); diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 7f619f1e..44de8c35 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -733,12 +733,15 @@ namespace SourceGit.ViewModels visible.Add(commit); break; case 1: - visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByUser, _onlySearchCommitsInCurrentBranch).Result(); + visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByAuthor, _onlySearchCommitsInCurrentBranch).Result(); break; case 2: - visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByMessage, _onlySearchCommitsInCurrentBranch).Result(); + visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByCommitter, _onlySearchCommitsInCurrentBranch).Result(); break; case 3: + visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByMessage, _onlySearchCommitsInCurrentBranch).Result(); + break; + case 4: visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByFile, _onlySearchCommitsInCurrentBranch).Result(); break; } @@ -936,7 +939,7 @@ namespace SourceGit.ViewModels RemoteBranchTrees = builder.Remotes; if (_workingCopy != null) - _workingCopy.CanCommitWithPush = _currentBranch != null && !string.IsNullOrEmpty(_currentBranch.Upstream); + _workingCopy.HasRemotes = remotes.Count > 0; }); } @@ -1196,6 +1199,25 @@ namespace SourceGit.ViewModels App.GetLauncer()?.OpenRepositoryInTab(node, null); } + public AvaloniaList GetPreferedOpenAIServices() + { + var services = Preferences.Instance.OpenAIServices; + if (services == null || services.Count == 0) + return []; + + if (services.Count == 1) + return services; + + var prefered = _settings.PreferedOpenAIService; + foreach (var service in services) + { + if (service.Name.Equals(prefered, StringComparison.Ordinal)) + return [service]; + } + + return services; + } + public ContextMenu CreateContextMenuForGitFlow() { var menu = new ContextMenu(); @@ -1497,7 +1519,7 @@ namespace SourceGit.ViewModels menu.Items.Add(fastForward); menu.Items.Add(pull); } - } + } menu.Items.Add(push); } @@ -1515,7 +1537,7 @@ namespace SourceGit.ViewModels }; menu.Items.Add(checkout); menu.Items.Add(new MenuItem() { Header = "-" }); - } + } var worktree = _worktrees.Find(x => x.Branch == branch.FullName); var upstream = _branches.Find(x => x.FullName == branch.Upstream); @@ -1574,7 +1596,7 @@ namespace SourceGit.ViewModels menu.Items.Add(merge); menu.Items.Add(rebase); - } + } var compareWithHead = new MenuItem(); compareWithHead.Header = App.Text("BranchCM.CompareWithHead"); @@ -1626,7 +1648,7 @@ namespace SourceGit.ViewModels menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(finish); } - } + } var rename = new MenuItem(); rename.Header = new Views.NameHighlightedTextBlock("BranchCM.Rename", branch.Name); @@ -1699,7 +1721,7 @@ namespace SourceGit.ViewModels }; menu.Items.Add(tracking); } - } + } var archive = new MenuItem(); archive.Icon = App.CreateMenuIcon("Icons.Archive"); @@ -2358,7 +2380,7 @@ namespace SourceGit.ViewModels Dispatcher.UIThread.Invoke(() => IsAutoFetching = true); foreach (var remote in remotes) - new Commands.Fetch(_fullpath, remote, false, _settings.EnablePruneOnFetch, false, null) { RaiseError = false }.Exec(); + new Commands.Fetch(_fullpath, remote, false, false, null) { RaiseError = false }.Exec(); _lastFetchTime = DateTime.Now; Dispatcher.UIThread.Invoke(() => IsAutoFetching = false); } @@ -2382,7 +2404,7 @@ namespace SourceGit.ViewModels private bool _isSearching = false; private bool _isSearchLoadingVisible = false; private bool _isSearchCommitSuggestionOpen = false; - private int _searchCommitFilterType = 2; + private int _searchCommitFilterType = 3; private bool _onlySearchCommitsInCurrentBranch = false; private string _searchCommitFilter = string.Empty; private List _searchedCommits = new List(); diff --git a/src/ViewModels/RepositoryConfigure.cs b/src/ViewModels/RepositoryConfigure.cs index 88a485bc..a7c04937 100644 --- a/src/ViewModels/RepositoryConfigure.cs +++ b/src/ViewModels/RepositoryConfigure.cs @@ -60,18 +60,6 @@ namespace SourceGit.ViewModels set => SetProperty(ref _httpProxy, value); } - public bool EnableSignOffForCommit - { - get => _repo.Settings.EnableSignOffForCommit; - set => _repo.Settings.EnableSignOffForCommit = value; - } - - public bool EnablePruneOnFetch - { - get => _repo.Settings.EnablePruneOnFetch; - set => _repo.Settings.EnablePruneOnFetch = value; - } - public bool EnableAutoFetch { get => _repo.Settings.EnableAutoFetch; diff --git a/src/ViewModels/StashChanges.cs b/src/ViewModels/StashChanges.cs index fd937bdd..33ebb1f3 100644 --- a/src/ViewModels/StashChanges.cs +++ b/src/ViewModels/StashChanges.cs @@ -36,6 +36,12 @@ namespace SourceGit.ViewModels set => _repo.Settings.KeepIndexWhenStash = value; } + public bool AutoRestore + { + get => _repo.Settings.AutoRestoreAfterStash; + set => _repo.Settings.AutoRestoreAfterStash = value; + } + public StashChanges(Repository repo, List changes, bool hasSelectedFiles) { _repo = repo; @@ -84,6 +90,9 @@ namespace SourceGit.ViewModels succ = StashWithChanges(_changes); } + if (AutoRestore && succ) + succ = new Commands.Stash(_repo.FullPath).Apply("stash@{0}", true); + CallUIThread(() => { _repo.MarkWorkingCopyDirtyManually(); diff --git a/src/ViewModels/StashesPage.cs b/src/ViewModels/StashesPage.cs index e5755a91..4a3bf933 100644 --- a/src/ViewModels/StashesPage.cs +++ b/src/ViewModels/StashesPage.cs @@ -141,15 +141,9 @@ namespace SourceGit.ViewModels apply.Header = App.Text("StashCM.Apply"); apply.Click += (_, ev) => { - Task.Run(() => new Commands.Stash(_repo.FullPath).Apply(stash.Name)); - ev.Handled = true; - }; + if (_repo.CanCreatePopup()) + _repo.ShowPopup(new ApplyStash(_repo.FullPath, stash)); - var pop = new MenuItem(); - pop.Header = App.Text("StashCM.Pop"); - pop.Click += (_, ev) => - { - Task.Run(() => new Commands.Stash(_repo.FullPath).Pop(stash.Name)); ev.Handled = true; }; @@ -165,7 +159,6 @@ namespace SourceGit.ViewModels var menu = new ContextMenu(); menu.Items.Add(apply); - menu.Items.Add(pop); menu.Items.Add(drop); return menu; } diff --git a/src/ViewModels/UpdateSubmodules.cs b/src/ViewModels/UpdateSubmodules.cs index d1c433e2..3553d1b5 100644 --- a/src/ViewModels/UpdateSubmodules.cs +++ b/src/ViewModels/UpdateSubmodules.cs @@ -56,25 +56,24 @@ namespace SourceGit.ViewModels { _repo.SetWatcherEnabled(false); - string target = string.Empty; + List targets; if (_updateAll) - { - ProgressDescription = "Updating submodules ..."; - } + targets = Submodules; else - { - target = SelectedSubmodule; - ProgressDescription = $"Updating submodule {target} ..."; - } + targets = [SelectedSubmodule]; return Task.Run(() => { - new Commands.Submodule(_repo.FullPath).Update( - target, - EnableInit, - EnableRecursive, - EnableRemote, - SetProgressDescription); + foreach (var submodule in targets) + { + ProgressDescription = $"Updating submodule {submodule} ..."; + new Commands.Submodule(_repo.FullPath).Update( + submodule, + EnableInit, + EnableRecursive, + EnableRemote, + SetProgressDescription); + } CallUIThread(() => _repo.SetWatcherEnabled(true)); return true; diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 67fd6990..ecb3c0d6 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -26,10 +26,10 @@ namespace SourceGit.ViewModels } } - public bool CanCommitWithPush + public bool HasRemotes { - get => _canCommitWithPush; - set => SetProperty(ref _canCommitWithPush, value); + get => _hasRemotes; + set => SetProperty(ref _hasRemotes, value); } public bool HasUnsolvedConflicts @@ -62,6 +62,12 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _isCommitting, value); } + public bool EnableSignOff + { + get => _repo.Settings.EnableSignOffForCommit; + set => _repo.Settings.EnableSignOffForCommit = value; + } + public bool UseAmend { get => _useAmend; @@ -93,12 +99,34 @@ namespace SourceGit.ViewModels } } + public string UnstagedFilter + { + get => _unstagedFilter; + set + { + if (SetProperty(ref _unstagedFilter, value)) + { + if (_isLoadingData) + return; + + VisibleUnstaged = GetVisibleUnstagedChanges(); + SelectedUnstaged = []; + } + } + } + public List Unstaged { get => _unstaged; private set => SetProperty(ref _unstaged, value); } + public List VisibleUnstaged + { + get => _visibleUnstaged; + private set => SetProperty(ref _visibleUnstaged, value); + } + public List Staged { get => _staged; @@ -185,8 +213,9 @@ namespace SourceGit.ViewModels _selectedStaged.Clear(); OnPropertyChanged(nameof(SelectedStaged)); + _visibleUnstaged.Clear(); _unstaged.Clear(); - OnPropertyChanged(nameof(Unstaged)); + OnPropertyChanged(nameof(VisibleUnstaged)); _staged.Clear(); OnPropertyChanged(nameof(Staged)); @@ -212,7 +241,7 @@ namespace SourceGit.ViewModels var inProgress = null as InProgressContext; if (File.Exists(Path.Combine(_repo.GitDir, "CHERRY_PICK_HEAD"))) inProgress = new CherryPickInProgress(_repo); - else if (File.Exists(Path.Combine(_repo.GitDir, "REBASE_HEAD")) && Directory.Exists(Path.Combine(_repo.GitDir, "rebase-merge"))) + else if (Directory.Exists(Path.Combine(_repo.GitDir, "rebase-merge")) || Directory.Exists(Path.Combine(_repo.GitDir, "rebase-apply"))) inProgress = new RebaseInProgress(_repo); else if (File.Exists(Path.Combine(_repo.GitDir, "REVERT_HEAD"))) inProgress = new RevertInProgress(_repo); @@ -243,7 +272,6 @@ namespace SourceGit.ViewModels } var unstaged = new List(); - var selectedUnstaged = new List(); var hasConflict = false; foreach (var c in changes) { @@ -251,12 +279,19 @@ namespace SourceGit.ViewModels { unstaged.Add(c); hasConflict |= c.IsConflit; - - if (lastSelectedUnstaged.Contains(c.Path)) - selectedUnstaged.Add(c); } } + _unstaged = unstaged; + + var visibleUnstaged = GetVisibleUnstagedChanges(); + var selectedUnstaged = new List(); + foreach (var c in visibleUnstaged) + { + if (lastSelectedUnstaged.Contains(c.Path)) + selectedUnstaged.Add(c); + } + var staged = GetStagedChanges(); var selectedStaged = new List(); foreach (var c in staged) @@ -269,7 +304,7 @@ namespace SourceGit.ViewModels { _isLoadingData = true; HasUnsolvedConflicts = hasConflict; - Unstaged = unstaged; + VisibleUnstaged = visibleUnstaged; Staged = staged; SelectedUnstaged = selectedUnstaged; SelectedStaged = selectedStaged; @@ -285,7 +320,7 @@ namespace SourceGit.ViewModels var inProgress = null as InProgressContext; if (File.Exists(Path.Combine(_repo.GitDir, "CHERRY_PICK_HEAD"))) inProgress = new CherryPickInProgress(_repo); - else if (File.Exists(Path.Combine(_repo.GitDir, "REBASE_HEAD")) && Directory.Exists(Path.Combine(_repo.GitDir, "rebase-merge"))) + else if (Directory.Exists(Path.Combine(_repo.GitDir, "rebase-merge")) || Directory.Exists(Path.Combine(_repo.GitDir, "rebase-apply"))) inProgress = new RebaseInProgress(_repo); else if (File.Exists(Path.Combine(_repo.GitDir, "REVERT_HEAD"))) inProgress = new RevertInProgress(_repo); @@ -330,46 +365,7 @@ namespace SourceGit.ViewModels public void StageAll() { - StageChanges(_unstaged, null); - } - - public async void StageChanges(List changes, Models.Change next) - { - if (_unstaged.Count == 0 || changes.Count == 0) - return; - - // Use `_selectedUnstaged` instead of `SelectedUnstaged` to avoid UI refresh. - _selectedUnstaged = next != null ? [next] : []; - - IsStaging = true; - _repo.SetWatcherEnabled(false); - if (changes.Count == _unstaged.Count) - { - 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) - { - var count = Math.Min(10, changes.Count - i); - var step = changes.GetRange(i, count); - await Task.Run(() => new Commands.Add(_repo.FullPath, step).Exec()); - } - } - _repo.MarkWorkingCopyDirtyManually(); - _repo.SetWatcherEnabled(true); - IsStaging = false; + StageChanges(_visibleUnstaged, null); } public void UnstageSelected(Models.Change next) @@ -382,47 +378,15 @@ namespace SourceGit.ViewModels UnstageChanges(_staged, null); } - public async void UnstageChanges(List changes, Models.Change next) - { - if (_staged.Count == 0 || changes.Count == 0) - return; - - // Use `_selectedStaged` instead of `SelectedStaged` to avoid UI refresh. - _selectedStaged = next != null ? [next] : []; - - IsUnstaging = true; - _repo.SetWatcherEnabled(false); - if (_useAmend) - { - await Task.Run(() => new Commands.UnstageChangesForAmend(_repo.FullPath, changes).Exec()); - } - else if (changes.Count == _staged.Count) - { - await Task.Run(() => new Commands.Reset(_repo.FullPath).Exec()); - } - else - { - for (int i = 0; i < changes.Count; i += 10) - { - var count = Math.Min(10, changes.Count - i); - var step = changes.GetRange(i, count); - await Task.Run(() => new Commands.Reset(_repo.FullPath, step).Exec()); - } - } - _repo.MarkWorkingCopyDirtyManually(); - _repo.SetWatcherEnabled(true); - IsUnstaging = false; - } - public void Discard(List changes) { if (_repo.CanCreatePopup()) - { - if (changes.Count == _unstaged.Count && _staged.Count == 0) - _repo.ShowPopup(new Discard(_repo)); - else - _repo.ShowPopup(new Discard(_repo, changes)); - } + _repo.ShowPopup(new Discard(_repo, changes)); + } + + public void ClearUnstagedFilter() + { + UnstagedFilter = string.Empty; } public async void UseTheirs(List changes) @@ -1067,7 +1031,7 @@ namespace SourceGit.ViewModels var menu = new ContextMenu(); var ai = null as MenuItem; - var services = GetPreferedOpenAIServices(); + var services = _repo.GetPreferedOpenAIServices(); if (services.Count > 0) { ai = new MenuItem(); @@ -1078,7 +1042,7 @@ namespace SourceGit.ViewModels { ai.Click += (_, e) => { - var dialog = new Views.AIAssistant(services[0], _repo.FullPath, _selectedStaged, generated => CommitMessage = generated); + var dialog = new Views.AIAssistant(services[0], _repo.FullPath, this, _selectedStaged); App.OpenDialog(dialog); e.Handled = true; }; @@ -1093,7 +1057,7 @@ namespace SourceGit.ViewModels item.Header = service.Name; item.Click += (_, e) => { - var dialog = new Views.AIAssistant(dup, _repo.FullPath, _selectedStaged, generated => CommitMessage = generated); + var dialog = new Views.AIAssistant(dup, _repo.FullPath, this, _selectedStaged); App.OpenDialog(dialog); e.Handled = true; }; @@ -1458,7 +1422,7 @@ namespace SourceGit.ViewModels return null; } - var services = GetPreferedOpenAIServices(); + var services = _repo.GetPreferedOpenAIServices(); if (services.Count == 0) { App.RaiseException(_repo.FullPath, "Bad configuration for OpenAI"); @@ -1467,7 +1431,7 @@ namespace SourceGit.ViewModels if (services.Count == 1) { - var dialog = new Views.AIAssistant(services[0], _repo.FullPath, _staged, generated => CommitMessage = generated); + var dialog = new Views.AIAssistant(services[0], _repo.FullPath, this, _staged); App.OpenDialog(dialog); return null; } @@ -1483,7 +1447,7 @@ namespace SourceGit.ViewModels item.Header = service.Name; item.Click += (_, e) => { - var dialog = new Views.AIAssistant(dup, _repo.FullPath, _staged, generated => CommitMessage = generated); + var dialog = new Views.AIAssistant(dup, _repo.FullPath, this, _staged); App.OpenDialog(dialog); e.Handled = true; }; @@ -1495,6 +1459,22 @@ namespace SourceGit.ViewModels } } + private List GetVisibleUnstagedChanges() + { + if (string.IsNullOrEmpty(_unstagedFilter)) + return _unstaged; + + var visible = new List(); + + foreach (var c in _unstaged) + { + if (c.Path.Contains(_unstagedFilter, StringComparison.OrdinalIgnoreCase)) + visible.Add(c); + } + + return visible; + } + private List GetStagedChanges() { if (_useAmend) @@ -1510,6 +1490,77 @@ namespace SourceGit.ViewModels return rs; } + private async void StageChanges(List changes, Models.Change next) + { + if (changes.Count == 0) + return; + + // Use `_selectedUnstaged` instead of `SelectedUnstaged` to avoid UI refresh. + _selectedUnstaged = next != null ? [next] : []; + + IsStaging = true; + _repo.SetWatcherEnabled(false); + if (changes.Count == _unstaged.Count) + { + 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) + { + var count = Math.Min(10, changes.Count - i); + var step = changes.GetRange(i, count); + await Task.Run(() => new Commands.Add(_repo.FullPath, step).Exec()); + } + } + _repo.MarkWorkingCopyDirtyManually(); + _repo.SetWatcherEnabled(true); + IsStaging = false; + } + + private async void UnstageChanges(List changes, Models.Change next) + { + if (changes.Count == 0) + return; + + // Use `_selectedStaged` instead of `SelectedStaged` to avoid UI refresh. + _selectedStaged = next != null ? [next] : []; + + IsUnstaging = true; + _repo.SetWatcherEnabled(false); + if (_useAmend) + { + await Task.Run(() => new Commands.UnstageChangesForAmend(_repo.FullPath, changes).Exec()); + } + else if (changes.Count == _staged.Count) + { + await Task.Run(() => new Commands.Reset(_repo.FullPath).Exec()); + } + else + { + for (int i = 0; i < changes.Count; i += 10) + { + var count = Math.Min(10, changes.Count - i); + var step = changes.GetRange(i, count); + await Task.Run(() => new Commands.Reset(_repo.FullPath, step).Exec()); + } + } + _repo.MarkWorkingCopyDirtyManually(); + _repo.SetWatcherEnabled(true); + IsUnstaging = false; + } + private void SetDetail(Models.Change change, bool isUnstaged) { if (_isLoadingData) @@ -1599,39 +1650,22 @@ namespace SourceGit.ViewModels return false; } - private IList GetPreferedOpenAIServices() - { - var services = Preferences.Instance.OpenAIServices; - if (services == null || services.Count == 0) - return []; - - if (services.Count == 1) - return services; - - var prefered = _repo.Settings.PreferedOpenAIService; - foreach (var service in services) - { - if (service.Name.Equals(prefered, StringComparison.Ordinal)) - return [service]; - } - - return services; - } - private Repository _repo = null; private bool _isLoadingData = false; private bool _isStaging = false; private bool _isUnstaging = false; private bool _isCommitting = false; private bool _useAmend = false; - private bool _canCommitWithPush = false; + private bool _hasRemotes = false; private List _cached = []; private List _unstaged = []; + private List _visibleUnstaged = []; private List _staged = []; private List _selectedUnstaged = []; private List _selectedStaged = []; private int _count = 0; private object _detailContext = null; + private string _unstagedFilter = string.Empty; private string _commitMessage = string.Empty; private bool _hasUnsolvedConflicts = false; diff --git a/src/Views/AIAssistant.axaml b/src/Views/AIAssistant.axaml index a273b240..e07c3a3e 100644 --- a/src/Views/AIAssistant.axaml +++ b/src/Views/AIAssistant.axaml @@ -10,7 +10,7 @@ x:Name="ThisControl" Icon="/App.ico" Title="{DynamicResource Text.AIAssistant}" - Width="400" SizeToContent="Height" + Width="520" SizeToContent="Height" CanResize="False" WindowStartupLocation="CenterOwner"> @@ -36,18 +36,33 @@ IsVisible="{OnPlatform True, macOS=False}"/> - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Views/LauncherTabBar.axaml.cs b/src/Views/LauncherTabBar.axaml.cs index f8c9107c..129ce892 100644 --- a/src/Views/LauncherTabBar.axaml.cs +++ b/src/Views/LauncherTabBar.axaml.cs @@ -43,6 +43,9 @@ namespace SourceGit.Views if (containerEndX < startX || containerEndX > endX) continue; + if (OuterNewTabBtn.IsVisible && i == count - 1) + break; + var separatorX = containerEndX - startX + LauncherTabsScroller.Bounds.X; context.DrawLine(separatorPen, new Point(separatorX, separatorY), new Point(separatorX, separatorY + 20)); } @@ -88,7 +91,7 @@ namespace SourceGit.Views x = drawRightX - 6; } - if (drawRightX < LauncherTabsScroller.Bounds.Right) + if (drawRightX <= LauncherTabsScroller.Bounds.Right) { ctx.LineTo(new Point(x, y)); x = drawRightX; @@ -146,11 +149,15 @@ namespace SourceGit.Views LeftScrollIndicator.IsEnabled = LauncherTabsScroller.Offset.X > 0; RightScrollIndicator.IsVisible = true; RightScrollIndicator.IsEnabled = LauncherTabsScroller.Offset.X < LauncherTabsScroller.Extent.Width - LauncherTabsScroller.Viewport.Width; + InnerNewTabBtn.IsVisible = false; + OuterNewTabBtn.IsVisible = true; } else { LeftScrollIndicator.IsVisible = false; RightScrollIndicator.IsVisible = false; + InnerNewTabBtn.IsVisible = true; + OuterNewTabBtn.IsVisible = false; } InvalidateVisual(); diff --git a/src/Views/MenuItemExtension.cs b/src/Views/MenuItemExtension.cs new file mode 100644 index 00000000..1c23b2ea --- /dev/null +++ b/src/Views/MenuItemExtension.cs @@ -0,0 +1,12 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data; + +namespace SourceGit.Views +{ + public class MenuItemExtension : AvaloniaObject + { + public static readonly AttachedProperty CommandProperty = + AvaloniaProperty.RegisterAttached("Command", string.Empty, false, BindingMode.OneWay); + } +} diff --git a/src/Views/Preferences.axaml b/src/Views/Preferences.axaml index 7f3633ad..1d282ad9 100644 --- a/src/Views/Preferences.axaml +++ b/src/Views/Preferences.axaml @@ -255,7 +255,7 @@ - + + + diff --git a/src/Views/Preferences.axaml.cs b/src/Views/Preferences.axaml.cs index 2bc5c571..4696b4a7 100644 --- a/src/Views/Preferences.axaml.cs +++ b/src/Views/Preferences.axaml.cs @@ -28,6 +28,12 @@ namespace SourceGit.Views set; } = null; + public bool EnablePruneOnFetch + { + get; + set; + } + public static readonly StyledProperty GitVersionProperty = AvaloniaProperty.Register(nameof(GitVersion)); @@ -114,6 +120,8 @@ namespace SourceGit.Views GPGUserKey = signingKey; if (config.TryGetValue("core.autocrlf", out var crlf)) CRLFMode = Models.CRLFMode.Supported.Find(x => x.Value == crlf); + if (config.TryGetValue("fetch.prune", out var pruneOnFetch)) + EnablePruneOnFetch = (pruneOnFetch == "true"); if (config.TryGetValue("commit.gpgsign", out var gpgCommitSign)) EnableGPGCommitSigning = (gpgCommitSign == "true"); if (config.TryGetValue("tag.gpgsign", out var gpgTagSign)) @@ -157,6 +165,7 @@ namespace SourceGit.Views SetIfChanged(config, "user.email", DefaultEmail, ""); SetIfChanged(config, "user.signingkey", GPGUserKey, ""); SetIfChanged(config, "core.autocrlf", CRLFMode != null ? CRLFMode.Value : null, null); + SetIfChanged(config, "fetch.prune", EnablePruneOnFetch ? "true" : "false", "false"); SetIfChanged(config, "commit.gpgsign", EnableGPGCommitSigning ? "true" : "false", "false"); SetIfChanged(config, "tag.gpgsign", EnableGPGTagSigning ? "true" : "false", "false"); SetIfChanged(config, "http.sslverify", EnableHTTPSSLVerify ? "" : "false", ""); diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 00f9f6ce..74f628c6 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -479,7 +479,8 @@ SelectedIndex="{Binding SearchCommitFilterType, Mode=TwoWay}"> - + + @@ -597,11 +598,18 @@ - - + + - + + + + - - +