diff --git a/TRANSLATION.md b/TRANSLATION.md index facab0ab2..479f5188d 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -6,55 +6,19 @@ This document shows the translation status of each locale file in the repository ### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen) -### ![de__DE](https://img.shields.io/badge/de__DE-95.07%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-99.74%25-yellow)
Missing keys in de_DE.axaml -- Text.Bisect -- Text.Bisect.Abort -- Text.Bisect.Bad -- Text.Bisect.Detecting -- Text.Bisect.Good -- Text.Bisect.Skip -- Text.Bisect.WaitingForRange -- Text.BranchUpstreamInvalid -- Text.CommitCM.CopyAuthor -- Text.CommitCM.CopyCommitter -- Text.CommitCM.CopySubject -- Text.CommitMessageTextBox.SubjectCount -- Text.Configure.CustomAction.WaitForExit -- Text.Configure.Git.PreferredMergeMode -- Text.Configure.IssueTracker.AddSampleAzure -- Text.ConfirmEmptyCommit.Continue -- Text.ConfirmEmptyCommit.NoLocalChanges -- Text.ConfirmEmptyCommit.StageAllThenCommit -- Text.ConfirmEmptyCommit.WithLocalChanges -- Text.CopyFullPath -- Text.Diff.First -- Text.Diff.Last -- Text.Preferences.AI.Streaming -- Text.Preferences.Appearance.EditorTabWidth -- Text.Preferences.General.ShowTagsInGraph -- Text.Preferences.Git.IgnoreCRAtEOLInDiff -- Text.Repository.Search.ByContent -- Text.Repository.ViewLogs -- Text.StashCM.SaveAsPatch -- Text.ViewLogs -- Text.ViewLogs.Clear -- Text.ViewLogs.CopyLog -- Text.ViewLogs.Delete -- Text.WorkingCopy.ConfirmCommitWithFilter -- Text.WorkingCopy.Conflicts.OpenExternalMergeTool -- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts -- Text.WorkingCopy.Conflicts.UseMine -- Text.WorkingCopy.Conflicts.UseTheirs +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash
### ![es__ES](https://img.shields.io/badge/es__ES-%E2%88%9A-brightgreen) -### ![fr__FR](https://img.shields.io/badge/fr__FR-96.37%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-95.50%25-yellow)
Missing keys in fr_FR.axaml @@ -66,6 +30,7 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.Good - Text.Bisect.Skip - Text.Bisect.WaitingForRange +- Text.Checkout.RecurseSubmodules - Text.CommitCM.CopyAuthor - Text.CommitCM.CopyCommitter - Text.CommitCM.CopySubject @@ -75,9 +40,15 @@ This document shows the translation status of each locale file in the repository - Text.ConfirmEmptyCommit.NoLocalChanges - Text.ConfirmEmptyCommit.StageAllThenCommit - Text.ConfirmEmptyCommit.WithLocalChanges +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash - Text.Preferences.Git.IgnoreCRAtEOLInDiff +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName - Text.Repository.Search.ByContent - Text.Repository.ViewLogs +- Text.Repository.Visit - Text.ViewLogs - Text.ViewLogs.Clear - Text.ViewLogs.CopyLog @@ -90,7 +61,7 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-96.11%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-95.24%25-yellow)
Missing keys in it_IT.axaml @@ -102,6 +73,7 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.Good - Text.Bisect.Skip - Text.Bisect.WaitingForRange +- Text.Checkout.RecurseSubmodules - Text.CommitCM.CopyAuthor - Text.CommitCM.CopyCommitter - Text.CommitCM.CopySubject @@ -112,10 +84,16 @@ This document shows the translation status of each locale file in the repository - Text.ConfirmEmptyCommit.StageAllThenCommit - Text.ConfirmEmptyCommit.WithLocalChanges - Text.CopyFullPath +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash - Text.Preferences.General.ShowTagsInGraph - Text.Preferences.Git.IgnoreCRAtEOLInDiff +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName - Text.Repository.Search.ByContent - Text.Repository.ViewLogs +- Text.Repository.Visit - Text.ViewLogs - Text.ViewLogs.Clear - Text.ViewLogs.CopyLog @@ -128,7 +106,7 @@ This document shows the translation status of each locale file in the repository
-### ![ja__JP](https://img.shields.io/badge/ja__JP-96.11%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-95.24%25-yellow)
Missing keys in ja_JP.axaml @@ -140,6 +118,7 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.Good - Text.Bisect.Skip - Text.Bisect.WaitingForRange +- Text.Checkout.RecurseSubmodules - Text.CommitCM.CopyAuthor - Text.CommitCM.CopyCommitter - Text.CommitCM.CopySubject @@ -149,11 +128,17 @@ This document shows the translation status of each locale file in the repository - Text.ConfirmEmptyCommit.NoLocalChanges - Text.ConfirmEmptyCommit.StageAllThenCommit - Text.ConfirmEmptyCommit.WithLocalChanges +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash - Text.Preferences.Git.IgnoreCRAtEOLInDiff +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName - Text.Repository.FilterCommits - Text.Repository.Search.ByContent - Text.Repository.Tags.OrderByNameDes - Text.Repository.ViewLogs +- Text.Repository.Visit - Text.ViewLogs - Text.ViewLogs.Clear - Text.ViewLogs.CopyLog @@ -166,7 +151,7 @@ This document shows the translation status of each locale file in the repository
-### ![pt__BR](https://img.shields.io/badge/pt__BR-87.68%25-yellow) +### ![pt__BR](https://img.shields.io/badge/pt__BR-86.89%25-yellow)
Missing keys in pt_BR.axaml @@ -187,6 +172,7 @@ This document shows the translation status of each locale file in the repository - Text.BranchCM.CustomAction - Text.BranchCM.MergeMultiBranches - Text.BranchUpstreamInvalid +- Text.Checkout.RecurseSubmodules - Text.Clone.RecurseSubmodules - Text.CommitCM.CopyAuthor - Text.CommitCM.CopyCommitter @@ -215,6 +201,8 @@ This document shows the translation status of each locale file in the repository - Text.Diff.UseBlockNavigation - Text.Fetch.Force - Text.FileCM.ResolveUsing +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash - Text.Hotkeys.Global.Clone - Text.InProgress.CherryPick.Head - Text.InProgress.Merge.Operating @@ -232,6 +220,9 @@ This document shows the translation status of each locale file in the repository - Text.Preferences.General.ShowTagsInGraph - Text.Preferences.Git.IgnoreCRAtEOLInDiff - Text.Preferences.Git.SSLVerify +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName - Text.Repository.FilterCommits - Text.Repository.HistoriesLayout - Text.Repository.HistoriesLayout.Horizontal @@ -247,6 +238,7 @@ This document shows the translation status of each locale file in the repository - Text.Repository.Tags.Sort - Text.Repository.UseRelativeTimeInHistories - Text.Repository.ViewLogs +- Text.Repository.Visit - Text.SetUpstream - Text.SetUpstream.Local - Text.SetUpstream.Unset @@ -271,7 +263,7 @@ This document shows the translation status of each locale file in the repository ### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen) -### ![ta__IN](https://img.shields.io/badge/ta__IN-96.37%25-yellow) +### ![ta__IN](https://img.shields.io/badge/ta__IN-95.50%25-yellow)
Missing keys in ta_IN.axaml @@ -283,6 +275,7 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.Good - Text.Bisect.Skip - Text.Bisect.WaitingForRange +- Text.Checkout.RecurseSubmodules - Text.CommitCM.CopyAuthor - Text.CommitCM.CopyCommitter - Text.CommitCM.CopySubject @@ -292,9 +285,15 @@ This document shows the translation status of each locale file in the repository - Text.ConfirmEmptyCommit.NoLocalChanges - Text.ConfirmEmptyCommit.StageAllThenCommit - Text.ConfirmEmptyCommit.WithLocalChanges +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash - Text.Preferences.Git.IgnoreCRAtEOLInDiff +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName - Text.Repository.Search.ByContent - Text.Repository.ViewLogs +- Text.Repository.Visit - Text.UpdateSubmodules.Target - Text.ViewLogs - Text.ViewLogs.Clear @@ -307,7 +306,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-97.54%25-yellow) +### ![uk__UA](https://img.shields.io/badge/uk__UA-96.66%25-yellow)
Missing keys in uk_UA.axaml @@ -319,14 +318,21 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.Good - Text.Bisect.Skip - Text.Bisect.WaitingForRange +- Text.Checkout.RecurseSubmodules - Text.CommitCM.CopyAuthor - Text.CommitCM.CopyCommitter - Text.CommitCM.CopySubject - Text.CommitMessageTextBox.SubjectCount - Text.ConfigureWorkspace.Name +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash - Text.Preferences.Git.IgnoreCRAtEOLInDiff +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName - Text.Repository.Search.ByContent - Text.Repository.ViewLogs +- Text.Repository.Visit - Text.ViewLogs - Text.ViewLogs.Clear - Text.ViewLogs.CopyLog diff --git a/VERSION b/VERSION index fe39b2a2b..52dc0d52e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2025.16 \ No newline at end of file +2025.17 \ No newline at end of file diff --git a/src/App.axaml.cs b/src/App.axaml.cs index c54b616db..6e45164dc 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -78,7 +78,7 @@ public static AppBuilder BuildAvaloniaApp() return builder; } - private static void LogException(Exception ex) + public static void LogException(Exception ex) { if (ex == null) return; diff --git a/src/Commands/Checkout.cs b/src/Commands/Checkout.cs index c39c28aee..6f63ae603 100644 --- a/src/Commands/Checkout.cs +++ b/src/Commands/Checkout.cs @@ -11,15 +11,24 @@ public Checkout(string repo) Context = repo; } - public bool Branch(string branch) + public bool Branch(string branch, bool force) { - Args = $"checkout --recurse-submodules --progress {branch}"; + var option = force ? "--force" : string.Empty; + Args = $"checkout {option} --progress {branch}"; return Exec(); } - public bool Branch(string branch, string basedOn) + public bool Branch(string branch, string basedOn, bool force) { - Args = $"checkout --recurse-submodules --progress -b {branch} {basedOn}"; + var option = force ? "--force" : string.Empty; + Args = $"checkout --progress -b {branch} {basedOn}"; + return Exec(); + } + + public bool Commit(string commitId, bool force) + { + var option = force ? "--force" : string.Empty; + Args = $"checkout {option} --detach --progress {commitId}"; return Exec(); } @@ -56,11 +65,5 @@ public bool FileWithRevision(string file, string revision) Args = $"checkout --no-overlay {revision} -- \"{file}\""; return Exec(); } - - public bool Commit(string commitId) - { - Args = $"checkout --detach --progress {commitId}"; - return Exec(); - } } } diff --git a/src/Commands/Clean.cs b/src/Commands/Clean.cs index a10e58731..6ed749995 100644 --- a/src/Commands/Clean.cs +++ b/src/Commands/Clean.cs @@ -1,31 +1,12 @@ -using System.Collections.Generic; -using System.Text; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Clean : Command { - public Clean(string repo, bool includeIgnored) + public Clean(string repo) { WorkingDirectory = repo; Context = repo; - Args = includeIgnored ? "clean -qfdx" : "clean -qfd"; - } - - public Clean(string repo, List files) - { - var builder = new StringBuilder(); - builder.Append("clean -qfd --"); - foreach (var f in files) - { - builder.Append(" \""); - builder.Append(f); - builder.Append("\""); - } - - WorkingDirectory = repo; - Context = repo; - Args = builder.ToString(); + Args = "clean -qfdx"; } } } diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs index a354b8162..6af0a3cc6 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -28,11 +28,11 @@ public Diff(string repo, Models.DiffOption opt, int unified, bool ignoreWhitespa Context = repo; if (ignoreWhitespace) - Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --ignore-all-space --unified={unified} {opt}"; + Args = $"diff --no-ext-diff --patch --ignore-all-space --unified={unified} {opt}"; else if (Models.DiffOption.IgnoreCRAtEOL) - Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --ignore-cr-at-eol --unified={unified} {opt}"; + Args = $"diff --no-ext-diff --patch --ignore-cr-at-eol --unified={unified} {opt}"; else - Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --unified={unified} {opt}"; + Args = $"diff --no-ext-diff --patch --unified={unified} {opt}"; } public Models.DiffResult Result() diff --git a/src/Commands/Discard.cs b/src/Commands/Discard.cs index f837df528..fcbe85915 100644 --- a/src/Commands/Discard.cs +++ b/src/Commands/Discard.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.IO; + +using Avalonia.Threading; namespace SourceGit.Commands { @@ -7,33 +10,71 @@ public static class Discard { public static void All(string repo, bool includeIgnored, Models.ICommandLog log) { + var changes = new QueryLocalChanges(repo).Result(); + try + { + foreach (var c in changes) + { + if (c.WorkTree == Models.ChangeState.Untracked || + c.WorkTree == Models.ChangeState.Added || + c.Index == Models.ChangeState.Added || + c.Index == Models.ChangeState.Renamed) + { + var fullPath = Path.Combine(repo, c.Path); + if (Directory.Exists(fullPath)) + Directory.Delete(fullPath, true); + else + File.Delete(fullPath); + } + } + } + catch (Exception e) + { + Dispatcher.UIThread.Invoke(() => + { + App.RaiseException(repo, $"Failed to discard changes. Reason: {e.Message}"); + }); + } + new Restore(repo) { Log = log }.Exec(); - new Clean(repo, includeIgnored) { Log = log }.Exec(); + if (includeIgnored) + new Clean(repo) { Log = log }.Exec(); } public static void Changes(string repo, List changes, Models.ICommandLog log) { - var needClean = new List(); - var needCheckout = new List(); + var restores = new List(); - foreach (var c in changes) + try { - if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added) - needClean.Add(c.Path); - else - needCheckout.Add(c.Path); + foreach (var c in changes) + { + if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added) + { + var fullPath = Path.Combine(repo, c.Path); + if (Directory.Exists(fullPath)) + Directory.Delete(fullPath, true); + else + File.Delete(fullPath); + } + else + { + restores.Add(c.Path); + } + } } - - for (int i = 0; i < needClean.Count; i += 10) + catch (Exception e) { - var count = Math.Min(10, needClean.Count - i); - new Clean(repo, needClean.GetRange(i, count)) { Log = log }.Exec(); + Dispatcher.UIThread.Invoke(() => + { + App.RaiseException(repo, $"Failed to discard changes. Reason: {e.Message}"); + }); } - for (int i = 0; i < needCheckout.Count; i += 10) + for (int i = 0; i < restores.Count; i += 10) { - var count = Math.Min(10, needCheckout.Count - i); - new Restore(repo, needCheckout.GetRange(i, count), "--worktree --recurse-submodules") { Log = log }.Exec(); + var count = Math.Min(10, restores.Count - i); + new Restore(repo, restores.GetRange(i, count), "--worktree --recurse-submodules") { Log = log }.Exec(); } } } diff --git a/src/Commands/GitFlow.cs b/src/Commands/GitFlow.cs index 833d268d0..e4fab2355 100644 --- a/src/Commands/GitFlow.cs +++ b/src/Commands/GitFlow.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text; using Avalonia.Threading; @@ -134,7 +135,7 @@ public static bool Start(string repo, string type, string name, Models.ICommandL return start.Exec(); } - public static bool Finish(string repo, string type, string name, bool keepBranch, Models.ICommandLog log) + public static bool Finish(string repo, string type, string name, bool squash, bool push, bool keepBranch, Models.ICommandLog log) { if (!SUPPORTED_BRANCH_TYPES.Contains(type)) { @@ -146,11 +147,22 @@ public static bool Finish(string repo, string type, string name, bool keepBranch return false; } - var option = keepBranch ? "-k" : string.Empty; + var builder = new StringBuilder(); + builder.Append("flow "); + builder.Append(type); + builder.Append(" finish "); + if (squash) + builder.Append("--squash "); + if (push) + builder.Append("--push "); + if (keepBranch) + builder.Append("-k "); + builder.Append(name); + var finish = new Command(); finish.WorkingDirectory = repo; finish.Context = repo; - finish.Args = $"flow {type} finish {option} {name}"; + finish.Args = builder.ToString(); finish.Log = log; return finish.Exec(); } diff --git a/src/Commands/QueryBranches.cs b/src/Commands/QueryBranches.cs index 39b771892..19514954e 100644 --- a/src/Commands/QueryBranches.cs +++ b/src/Commands/QueryBranches.cs @@ -14,7 +14,7 @@ public QueryBranches(string repo) { WorkingDirectory = repo; Context = repo; - Args = "branch -l --all -v --format=\"%(refname)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(upstream:trackshort)\""; + Args = "branch -l --all -v --format=\"%(refname)%00%(committerdate:unix)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(upstream:trackshort)\""; } public List Result() @@ -49,7 +49,7 @@ public QueryBranches(string repo) private Models.Branch ParseLine(string line) { var parts = line.Split('\0'); - if (parts.Length != 5) + if (parts.Length != 6) return null; var branch = new Models.Branch(); @@ -83,12 +83,13 @@ private Models.Branch ParseLine(string line) } branch.FullName = refName; - branch.Head = parts[1]; - branch.IsCurrent = parts[2] == "*"; - branch.Upstream = parts[3]; + branch.CommitterDate = ulong.Parse(parts[1]); + branch.Head = parts[2]; + branch.IsCurrent = parts[3] == "*"; + branch.Upstream = parts[4]; branch.IsUpstreamGone = false; - if (branch.IsLocal && !string.IsNullOrEmpty(parts[4]) && !parts[4].Equals("=", StringComparison.Ordinal)) + if (branch.IsLocal && !string.IsNullOrEmpty(parts[5]) && !parts[5].Equals("=", StringComparison.Ordinal)) branch.TrackStatus = new QueryTrackStatus(WorkingDirectory, branch.Name, branch.Upstream).Result(); else branch.TrackStatus = new Models.BranchTrackStatus(); diff --git a/src/Commands/Submodule.cs b/src/Commands/Submodule.cs index e4f35fca8..c8b676ea5 100644 --- a/src/Commands/Submodule.cs +++ b/src/Commands/Submodule.cs @@ -1,4 +1,7 @@ -namespace SourceGit.Commands +using System.Collections.Generic; +using System.Text; + +namespace SourceGit.Commands { public class Submodule : Command { @@ -42,6 +45,28 @@ public bool Update(string module, bool init, bool recursive, bool useRemote) return Exec(); } + public bool Update(List modules, bool init, bool recursive, bool useRemote) + { + var builder = new StringBuilder(); + builder.Append("submodule update"); + + if (init) + builder.Append(" --init"); + if (recursive) + builder.Append(" --recursive"); + if (useRemote) + builder.Append(" --remote"); + if (modules.Count > 0) + { + builder.Append(" --"); + foreach (var module in modules) + builder.Append($" \"{module.Path}\""); + } + + Args = builder.ToString(); + return Exec(); + } + public bool Delete(string relativePath) { Args = $"submodule deinit -f \"{relativePath}\""; diff --git a/src/Models/Branch.cs b/src/Models/Branch.cs index d0ac19907..7146da3fa 100644 --- a/src/Models/Branch.cs +++ b/src/Models/Branch.cs @@ -23,10 +23,17 @@ public override string ToString() } } + public enum BranchSortMode + { + Name = 0, + CommitterDate, + } + public class Branch { public string Name { get; set; } public string FullName { get; set; } + public ulong CommitterDate { get; set; } public string Head { get; set; } public bool IsLocal { get; set; } public bool IsCurrent { get; set; } diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index 772097513..01488656a 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -64,8 +64,8 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable { const double unitWidth = 12; const double halfWidth = 6; - const double unitHeight = 28; - const double halfHeight = 14; + const double unitHeight = 1; + const double halfHeight = 0.5; var temp = new CommitGraph(); var unsolved = new List(); diff --git a/src/Models/CommitLink.cs b/src/Models/CommitLink.cs index 544ee3c33..2891e5d65 100644 --- a/src/Models/CommitLink.cs +++ b/src/Models/CommitLink.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace SourceGit.Models @@ -22,24 +22,24 @@ public static List Get(List remotes) { if (remote.TryGetVisitURL(out var url)) { - var trimmedUrl = url; + var trimmedUrl = url.AsSpan(); if (url.EndsWith(".git")) - trimmedUrl = url.Substring(0, url.Length - 4); + trimmedUrl = url.AsSpan(0, url.Length - 4); if (url.StartsWith("https://github.com/", StringComparison.Ordinal)) - outs.Add(new($"Github ({trimmedUrl.Substring(19)})", $"{url}/commit/")); + outs.Add(new($"Github ({trimmedUrl.Slice(19)})", $"{url}/commit/")); else if (url.StartsWith("https://gitlab.", StringComparison.Ordinal)) - outs.Add(new($"GitLab ({trimmedUrl.Substring(trimmedUrl.Substring(15).IndexOf('/') + 16)})", $"{url}/-/commit/")); + outs.Add(new($"GitLab ({trimmedUrl.Slice(trimmedUrl.Slice(15).IndexOf('/') + 16)})", $"{url}/-/commit/")); else if (url.StartsWith("https://gitee.com/", StringComparison.Ordinal)) - outs.Add(new($"Gitee ({trimmedUrl.Substring(18)})", $"{url}/commit/")); + outs.Add(new($"Gitee ({trimmedUrl.Slice(18)})", $"{url}/commit/")); else if (url.StartsWith("https://bitbucket.org/", StringComparison.Ordinal)) - outs.Add(new($"BitBucket ({trimmedUrl.Substring(22)})", $"{url}/commits/")); + outs.Add(new($"BitBucket ({trimmedUrl.Slice(22)})", $"{url}/commits/")); else if (url.StartsWith("https://codeberg.org/", StringComparison.Ordinal)) - outs.Add(new($"Codeberg ({trimmedUrl.Substring(21)})", $"{url}/commit/")); + outs.Add(new($"Codeberg ({trimmedUrl.Slice(21)})", $"{url}/commit/")); else if (url.StartsWith("https://gitea.org/", StringComparison.Ordinal)) - outs.Add(new($"Gitea ({trimmedUrl.Substring(18)})", $"{url}/commit/")); + outs.Add(new($"Gitea ({trimmedUrl.Slice(18)})", $"{url}/commit/")); else if (url.StartsWith("https://git.sr.ht/", StringComparison.Ordinal)) - outs.Add(new($"sourcehut ({trimmedUrl.Substring(18)})", $"{url}/commit/")); + outs.Add(new($"sourcehut ({trimmedUrl.Slice(18)})", $"{url}/commit/")); } } diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs index 34a720333..9af032bb1 100644 --- a/src/Models/RepositorySettings.cs +++ b/src/Models/RepositorySettings.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; @@ -38,6 +38,18 @@ public bool OnlyHighlighCurrentBranchInHistories set; } = false; + public BranchSortMode LocalBranchSortMode + { + get; + set; + } = BranchSortMode.Name; + + public BranchSortMode RemoteBranchSortMode + { + get; + set; + } = BranchSortMode.Name; + public TagSortMode TagSortMode { get; @@ -110,6 +122,12 @@ public bool CheckoutBranchOnCreateBranch set; } = true; + public bool UpdateSubmodulesOnCheckoutBranch + { + get; + set; + } = true; + public AvaloniaList HistoriesFilters { get; @@ -326,28 +344,28 @@ public string BuildHistoriesFilter() if (filter.Mode == FilterMode.Included) includedRefs.Add(filter.Pattern); else if (filter.Mode == FilterMode.Excluded) - excludedBranches.Add($"--exclude=\"{filter.Pattern.Substring(11)}\" --decorate-refs-exclude=\"{filter.Pattern}\""); + excludedBranches.Add($"--exclude=\"{filter.Pattern.AsSpan(11)}\" --decorate-refs-exclude=\"{filter.Pattern}\""); } else if (filter.Type == FilterType.LocalBranchFolder) { if (filter.Mode == FilterMode.Included) - includedRefs.Add($"--branches={filter.Pattern.Substring(11)}/*"); + includedRefs.Add($"--branches={filter.Pattern.AsSpan(11)}/*"); else if (filter.Mode == FilterMode.Excluded) - excludedBranches.Add($"--exclude=\"{filter.Pattern.Substring(11)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\""); + excludedBranches.Add($"--exclude=\"{filter.Pattern.AsSpan(11)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\""); } else if (filter.Type == FilterType.RemoteBranch) { if (filter.Mode == FilterMode.Included) includedRefs.Add(filter.Pattern); else if (filter.Mode == FilterMode.Excluded) - excludedRemotes.Add($"--exclude=\"{filter.Pattern.Substring(13)}\" --decorate-refs-exclude=\"{filter.Pattern}\""); + excludedRemotes.Add($"--exclude=\"{filter.Pattern.AsSpan(13)}\" --decorate-refs-exclude=\"{filter.Pattern}\""); } else if (filter.Type == FilterType.RemoteBranchFolder) { if (filter.Mode == FilterMode.Included) - includedRefs.Add($"--remotes={filter.Pattern.Substring(13)}/*"); + includedRefs.Add($"--remotes={filter.Pattern.AsSpan(13)}/*"); else if (filter.Mode == FilterMode.Excluded) - excludedRemotes.Add($"--exclude=\"{filter.Pattern.Substring(13)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\""); + excludedRemotes.Add($"--exclude=\"{filter.Pattern.AsSpan(13)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\""); } else if (filter.Type == FilterType.Tag) { diff --git a/src/Models/ShellOrTerminal.cs b/src/Models/ShellOrTerminal.cs index 3ada2cf99..7dfb22373 100644 --- a/src/Models/ShellOrTerminal.cs +++ b/src/Models/ShellOrTerminal.cs @@ -42,7 +42,8 @@ static ShellOrTerminal() new ShellOrTerminal("mac-terminal", "Terminal", ""), new ShellOrTerminal("iterm2", "iTerm", ""), new ShellOrTerminal("warp", "Warp", ""), - new ShellOrTerminal("ghostty", "Ghostty", "") + new ShellOrTerminal("ghostty", "Ghostty", ""), + new ShellOrTerminal("kitty", "kitty", "") }; } else @@ -58,6 +59,7 @@ static ShellOrTerminal() new ShellOrTerminal("foot", "Foot", "foot"), new ShellOrTerminal("wezterm", "WezTerm", "wezterm"), new ShellOrTerminal("ptyxis", "Ptyxis", "ptyxis"), + new ShellOrTerminal("kitty", "kitty", "kitty"), new ShellOrTerminal("custom", "Custom", ""), }; } diff --git a/src/Native/MacOS.cs b/src/Native/MacOS.cs index d7ef47014..0966233f6 100644 --- a/src/Native/MacOS.cs +++ b/src/Native/MacOS.cs @@ -59,6 +59,8 @@ public string FindTerminal(Models.ShellOrTerminal shell) return "Warp"; case "ghostty": return "Ghostty"; + case "kitty": + return "kitty"; } return string.Empty; diff --git a/src/Resources/Images/ShellIcons/kitty.png b/src/Resources/Images/ShellIcons/kitty.png new file mode 100644 index 000000000..465c2863d Binary files /dev/null and b/src/Resources/Images/ShellIcons/kitty.png differ diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 8eb49a3ec..30750931a 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -40,6 +40,13 @@ KEINE ALS UNVERÄNDERT ANGENOMMENEN DATEIEN ENTFERNEN BINÄRE DATEI NICHT UNTERSTÜTZT!!! + Bisect + Abbrechen + Schlecht + Bisecting. Ist der aktuelle HEAD gut oder fehlerhaft? + Gut + Überspringen + Bisecting. Aktuellen Commit als gut oder schlecht markieren und einen anderen auschecken. Blame BLAME WIRD BEI DIESER DATEI NICHT UNTERSTÜTZT!!! Auschecken von ${0}$... @@ -62,6 +69,7 @@ Benenne ${0}$ um... Setze verfolgten Branch... Branch Vergleich + Ungültiger upstream! Bytes ABBRECHEN Auf Vorgänger-Revision zurücksetzen @@ -78,6 +86,7 @@ Lokale Änderungen: Verwerfen Stashen & wieder anwenden + Alle Submodule updaten Branch: Cherry Pick Quelle an Commit-Nachricht anhängen @@ -102,8 +111,11 @@ Mehrere cherry-picken Mit HEAD vergleichen Mit Worktree vergleichen + Author + Committer Information SHA + Betreff Benutzerdefinierte Aktion Interactives Rebase von ${0}$ auf diesen Commit Merge in ${0}$ hinein @@ -135,6 +147,7 @@ SHA Im Browser öffnen Details + Betreff Commit-Nachricht Repository Einstellungen COMMIT TEMPLATE @@ -149,13 +162,16 @@ Branch Commit Repository + Auf Beenden der Aktion warten Email Adresse Email Adresse GIT Remotes automatisch fetchen Minute(n) Standard Remote + Bevorzugter Merge Modus TICKETSYSTEM + Beispiel Azure DevOps Rule hinzufügen Beispiel für Gitee Issue Regel einfügen Beispiel für Gitee Pull Request Regel einfügen Beispiel für Github-Regel hinzufügen @@ -178,6 +194,10 @@ Farbe Name Zuletzt geöffnete Tabs beim Starten wiederherstellen + WEITER + Leerer Commit erkannt! Möchtest du trotzdem fortfahren (--allow-empty)? + ALLES STAGEN & COMMITTEN + Leerer Commit erkannt! Möchtest du trotzdem fortfahren (--allow-empty) oder alle Änderungen stagen und dann committen? Konventionelle Commit-Hilfe Breaking Change: Geschlossenes Ticket: @@ -187,6 +207,7 @@ Typ der Änderung: Kopieren Kopiere gesamten Text + Ganzen Pfad kopieren Pfad kopieren Branch erstellen... Basierend auf: @@ -236,7 +257,9 @@ ALT Kopieren Dateimodus geändert + Erste Differenz Ignoriere Leerzeichenänderungen + Letzte Differenz LFS OBJEKT ÄNDERUNG Nächste Änderung KEINE ÄNDERUNG ODER NUR ZEILEN-ENDE ÄNDERUNGEN @@ -447,8 +470,10 @@ Modell Name Server + Streaming aktivieren DARSTELLUNG Standardschriftart + Editor Tab Breite Schriftgröße Standard Texteditor @@ -469,6 +494,7 @@ Commit-Historie Zeige Autor Zeitpunkt anstatt Commit Zeitpunkt Zeige Nachfolger in den Commit Details + Zeige Tags im Commit Graph Längenvorgabe für Commit-Nachrichten GIT Aktiviere Auto-CRLF @@ -476,6 +502,7 @@ Benutzer Email Globale Git Benutzer Email Aktivere --prune beim fetchen + Aktiviere --ignore-cr-at-eol beim Unterschied Diese App setzt Git (>= 2.23.0) voraus Installationspfad Aktiviere HTTP SSL Verifizierung @@ -549,6 +576,9 @@ Branch: ABBRECHEN Änderungen automatisch von Remote fetchen... + Sortieren + Nach Commit Datum + Nach Name Aufräumen (GC & Prune) Führt `git gc` auf diesem Repository aus. Filter aufheben @@ -583,6 +613,7 @@ Commit suchen Autor Committer + Inhalt Dateiname Commit-Nachricht SHA @@ -601,6 +632,8 @@ Sortiere Öffne im Terminal Verwende relative Zeitangaben in Verlauf + Logs ansehen + Öffne '{0}' im Browser WORKTREES WORKTREE HINZUFÜGEN PRUNE @@ -651,6 +684,7 @@ Lokale Änderungen stashen Anwenden Entfernen + Als Path speichern... Stash entfernen Entfernen: Stashes @@ -685,6 +719,10 @@ Submodul: Verwende `--remote` Option URL: + Logs + ALLES LÖSCHEN + Kopieren + Löschen Warnung Willkommensseite Erstelle Gruppe @@ -714,8 +752,13 @@ Klick-Ereignis auslösen Commit (Bearbeitung) Alle Änderungen stagen und committen + Du hast {0} Datei(en) gestaged, aber nur {1} werden angezeigt ({2} sind herausgefiltert). Willst du trotzdem fortfahren? KONFLIKTE ERKANNT + EXTERNES MERGE-TOOL ÖFFNEN + ALLE KONFLIKTE IN EXTERNEM MERGE-TOOL ÖFFNEN DATEI KONFLIKTE GELÖST + MEINE VERSION VERWENDEN + DEREN VERSION VERWENDEN NICHT-VERFOLGTE DATEIEN INKLUDIEREN KEINE BISHERIGEN COMMIT-NACHRICHTEN KEINE COMMIT TEMPLATES diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index f4c62a90f..e2281a865 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -82,6 +82,7 @@ Local Changes: Discard Stash & Reapply + Update all submodules Branch: Cherry Pick Append source to commit message @@ -322,6 +323,8 @@ FLOW - Finish Hotfix FLOW - Finish Release Target: + Push to remote(s) after performing finish + Squash during merge Hotfix: Hotfix Prefix: Initialize Git-Flow @@ -571,6 +574,9 @@ Branch: ABORT Auto fetching changes from remotes... + Sort + By Committer Date + By Name Cleanup(GC & Prune) Run `git gc` command for this repository. Clear all @@ -601,7 +607,7 @@ Open in External Tools Refresh REMOTES - ADD REMOTE + Add Remote Search Commit Author Committer @@ -614,10 +620,10 @@ SKIP Statistics SUBMODULES - ADD SUBMODULE - UPDATE SUBMODULE + Add Submodule + Update Submodule TAGS - NEW TAG + New Tag By Creator Date By Name (Ascending) By Name (Descending) @@ -625,9 +631,10 @@ Open in Terminal Use relative time in histories View Logs + Visit '{0}' in Browser WORKTREES - ADD WORKTREE - PRUNE + Add Worktree + Prune Git Repository URL Reset Current Branch To Revision Reset Mode: diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index c9978b2a7..b11d0e98a 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -86,6 +86,7 @@ Cambios Locales: Descartar Stash & Reaplicar + Actualizar todos los submódulos Rama: Cherry Pick Añadir fuente al mensaje de commit @@ -101,7 +102,7 @@ Nombre Local: Nombre del repositorio. Opcional. Carpeta Padre: - Inicializar y actualizar submodulos + Inicializar y actualizar submódulos URL del Repositorio: CERRAR Editor @@ -326,6 +327,8 @@ FLOW - Finalizar Hotfix FLOW - Finalizar Release Destino: + Push al/los remoto(s) después de Finalizar + Squash durante el merge Hotfix: Prefijo de Hotfix: Inicializar Git-Flow @@ -575,6 +578,9 @@ Rama: ABORTAR Auto fetching cambios desde remotos... + Ordenar + Por Fecha de Committer + Por Nombre Limpiar (GC & Prune) Ejecutar comando `git gc` para este repositorio. Limpiar todo @@ -629,6 +635,7 @@ Abrir en Terminal Usar tiempo relativo en las historias Ver Logs + Visitar '{0}' en el Navegador WORKTREES AÑADIR WORKTREE PRUNE diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 4c8ea4bfc..1c45f22f1 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -86,6 +86,7 @@ Локальные изменения: Отклонить Отложить и примненить повторно + Обновить все подкаталоги Ветка: Частичный выбор Добавить источник для ревизии сообщения @@ -326,6 +327,8 @@ ПРОЦЕСС - Закончить исправление ПРОЦЕСС - Завершить выпуск Цель: + Выложить на удалённый(ые) после завершения + Втиснуть при слиянии Исправление: Префикс исправлений: Создать Git-процесс @@ -575,6 +578,9 @@ Ветка: Отказ Автоматическое извлечение изменений с внешних репозиторий... + Сортировать + По дате ревизора (исполнителя) + По имени Очистить (Сбор мусора и удаление) Запустить команду (git gc) для данного репозитория. Очистить всё @@ -628,7 +634,8 @@ Сортировать Открыть в терминале Использовать относительное время в историях - Просмотр логов + Просмотр журналов + Посетить '{0}' в браузере РАБОЧИЕ КАТАЛОГИ ДОБАВИТЬ РАБОЧИЙ КАТАЛОГ ОБРЕЗАТЬ @@ -714,7 +721,7 @@ Подмодуль: Использовать опцию (--remote) Сетевой адрес: - Логи + Журналы ОЧИСТИТЬ ВСЁ Копировать Удалить diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 8ee210e95..d08c6f703 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -86,6 +86,7 @@ 未提交更改 : 丢弃更改 贮藏并自动恢复 + 同时更新所有子模块 目标分支 : 挑选提交 提交信息中追加来源信息 @@ -326,6 +327,8 @@ 结束修复分支 结束版本分支 目标分支 : + 完成后自动推送 + 压缩变更为单一提交后合并分支 修复分支 : 修复分支名前缀 : 初始化GIT工作流 @@ -575,6 +578,9 @@ 分支 : 终止合并 自动拉取远端变更中... + 排序方式 + 按提交时间 + 按名称 清理本仓库(GC) 本操作将执行`git gc`命令。 清空过滤规则 @@ -629,6 +635,7 @@ 在终端中打开 在提交列表中使用相对时间 查看命令日志 + 访问远程仓库 '{0}' 工作树列表 新增工作树 清理 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 7430d609f..4cd68cadb 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -86,6 +86,7 @@ 未提交變更: 捨棄變更 擱置變更並自動復原 + 同時更新所有子模組 目標分支: 揀選提交 提交資訊中追加來源資訊 @@ -326,6 +327,8 @@ 完成修復分支 完成發行分支 目標分支: + 完成後自動推送 + 壓縮為單一提交後合併 修復分支: 修復分支前置詞: 初始化 Git 工作流 @@ -575,6 +578,9 @@ 分支: 中止 自動提取遠端變更中... + 排序 + 依建立時間 + 依名稱升序 清理本存放庫 (GC) 本操作將執行 `git gc` 命令。 清空篩選規則 @@ -629,6 +635,7 @@ 在終端機中開啟 在提交列表中使用相對時間 檢視 Git 指令記錄 + 檢視遠端存放庫 '{0}' 工作區列表 新增工作區 清理 diff --git a/src/Resources/Themes.axaml b/src/Resources/Themes.axaml index f33e71d2a..3194c8c3d 100644 --- a/src/Resources/Themes.axaml +++ b/src/Resources/Themes.axaml @@ -25,7 +25,7 @@ #A7E1A7 #F19B9D #0000EE - #FFE5E5E5 + #FFE4E4E4 @@ -52,7 +52,7 @@ #A0308D3C #A09F4247 #4DAAFC - #FF2E2E2E + #FF383838 diff --git a/src/ViewModels/BranchTreeNode.cs b/src/ViewModels/BranchTreeNode.cs index 0148844a0..d0289f6a3 100644 --- a/src/ViewModels/BranchTreeNode.cs +++ b/src/ViewModels/BranchTreeNode.cs @@ -10,6 +10,7 @@ public class BranchTreeNode : ObservableObject public string Name { get; private set; } = string.Empty; public string Path { get; private set; } = string.Empty; public object Backend { get; private set; } = null; + public ulong TimeToSort { get; private set; } = 0; public int Depth { get; set; } = 0; public bool IsSelected { get; set; } = false; public List Children { get; private set; } = new List(); @@ -62,6 +63,12 @@ public class Builder public List Remotes => _remotes; public List InvalidExpandedNodes => _invalidExpandedNodes; + public Builder(Models.BranchSortMode localSortMode, Models.BranchSortMode remoteSortMode) + { + _localSortMode = localSortMode; + _remoteSortMode = remoteSortMode; + } + public void SetExpandedNodes(List expanded) { foreach (var node in expanded) @@ -72,6 +79,7 @@ public void Run(List branches, List remotes, bool { var folders = new Dictionary(); + var fakeRemoteTime = (ulong)remotes.Count; foreach (var remote in remotes) { var path = $"refs/remotes/{remote.Name}"; @@ -81,8 +89,10 @@ public void Run(List branches, List remotes, bool Path = path, Backend = remote, IsExpanded = bForceExpanded || _expanded.Contains(path), + TimeToSort = fakeRemoteTime, }; + fakeRemoteTime--; folders.Add(path, node); _remotes.Add(node); } @@ -108,12 +118,21 @@ public void Run(List branches, List remotes, bool } folders.Clear(); - SortNodes(_locals); - SortNodes(_remotes); + + if (_localSortMode == Models.BranchSortMode.Name) + SortNodesByName(_locals); + else + SortNodesByTime(_locals); + + if (_remoteSortMode == Models.BranchSortMode.Name) + SortNodesByName(_remotes); + else + SortNodesByTime(_remotes); } private void MakeBranchNode(Models.Branch branch, List roots, Dictionary folders, string prefix, bool bForceExpanded) { + var time = branch.CommitterDate; var fullpath = $"{prefix}/{branch.Name}"; var sepIdx = branch.Name.IndexOf('/', StringComparison.Ordinal); if (sepIdx == -1 || branch.IsDetachedHead) @@ -124,6 +143,7 @@ private void MakeBranchNode(Models.Branch branch, List roots, Di Path = fullpath, Backend = branch, IsExpanded = false, + TimeToSort = time, }); return; } @@ -138,6 +158,7 @@ private void MakeBranchNode(Models.Branch branch, List roots, Di if (folders.TryGetValue(folder, out var val)) { lastFolder = val; + lastFolder.TimeToSort = Math.Max(lastFolder.TimeToSort, time); if (!lastFolder.IsExpanded) lastFolder.IsExpanded |= (branch.IsCurrent || _expanded.Contains(folder)); } @@ -148,6 +169,7 @@ private void MakeBranchNode(Models.Branch branch, List roots, Di Name = name, Path = folder, IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder), + TimeToSort = time, }; roots.Add(lastFolder); folders.Add(folder, lastFolder); @@ -159,6 +181,7 @@ private void MakeBranchNode(Models.Branch branch, List roots, Di Name = name, Path = folder, IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder), + TimeToSort = time, }; lastFolder.Children.Add(cur); folders.Add(folder, cur); @@ -175,10 +198,11 @@ private void MakeBranchNode(Models.Branch branch, List roots, Di Path = fullpath, Backend = branch, IsExpanded = false, + TimeToSort = time, }); } - private void SortNodes(List nodes) + private void SortNodesByName(List nodes) { nodes.Sort((l, r) => { @@ -192,9 +216,39 @@ private void SortNodes(List nodes) }); foreach (var node in nodes) - SortNodes(node.Children); + SortNodesByName(node.Children); + } + + private void SortNodesByTime(List nodes) + { + nodes.Sort((l, r) => + { + if (l.Backend is Models.Branch { IsDetachedHead: true }) + return -1; + + if (l.Backend is Models.Branch) + { + if (r.Backend is Models.Branch) + return r.TimeToSort == l.TimeToSort ? Models.NumericSort.Compare(l.Name, r.Name) : r.TimeToSort.CompareTo(l.TimeToSort); + else + return 1; + } + + if (r.Backend is Models.Branch) + return -1; + + if (r.TimeToSort == l.TimeToSort) + return Models.NumericSort.Compare(l.Name, r.Name); + + return r.TimeToSort.CompareTo(l.TimeToSort); + }); + + foreach (var node in nodes) + SortNodesByTime(node.Children); } + private readonly Models.BranchSortMode _localSortMode = Models.BranchSortMode.Name; + private readonly Models.BranchSortMode _remoteSortMode = Models.BranchSortMode.Name; private readonly List _locals = new List(); private readonly List _remotes = new List(); private readonly List _invalidExpandedNodes = new List(); diff --git a/src/ViewModels/Checkout.cs b/src/ViewModels/Checkout.cs index a96ac79a9..d8e9b38aa 100644 --- a/src/ViewModels/Checkout.cs +++ b/src/ViewModels/Checkout.cs @@ -15,11 +15,24 @@ public bool DiscardLocalChanges set; } + public bool IsRecurseSubmoduleVisible + { + get; + private set; + } + + public bool RecurseSubmodules + { + get => _repo.Settings.UpdateSubmodulesOnCheckoutBranch; + set => _repo.Settings.UpdateSubmodulesOnCheckoutBranch = value; + } + public Checkout(Repository repo, string branch) { _repo = repo; Branch = branch; DiscardLocalChanges = false; + IsRecurseSubmoduleVisible = repo.Submodules.Count > 0; } public override Task Sure() @@ -30,19 +43,22 @@ public override Task Sure() var log = _repo.CreateLog($"Checkout '{Branch}'"); Use(log); + var updateSubmodules = IsRecurseSubmoduleVisible && RecurseSubmodules; return Task.Run(() => { - var changes = new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).Result(); + var succ = false; var needPopStash = false; - if (changes > 0) + + if (DiscardLocalChanges) { - if (DiscardLocalChanges) - { - Commands.Discard.All(_repo.FullPath, false, log); - } - else + succ = new Commands.Checkout(_repo.FullPath).Use(log).Branch(Branch, true); + } + else + { + var changes = new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).Result(); + if (changes > 0) { - var succ = new Commands.Stash(_repo.FullPath).Use(log).Push("CHECKOUT_AUTO_STASH"); + succ = new Commands.Stash(_repo.FullPath).Use(log).Push("CHECKOUT_AUTO_STASH"); if (!succ) { log.Complete(); @@ -52,11 +68,22 @@ public override Task Sure() needPopStash = true; } + + succ = new Commands.Checkout(_repo.FullPath).Use(log).Branch(Branch, false); } - var rs = new Commands.Checkout(_repo.FullPath).Use(log).Branch(Branch); - if (needPopStash) - rs = new Commands.Stash(_repo.FullPath).Use(log).Pop("stash@{0}"); + if (succ) + { + if (updateSubmodules) + { + var submodules = new Commands.QuerySubmodules(_repo.FullPath).Result(); + if (submodules.Count > 0) + new Commands.Submodule(_repo.FullPath).Use(log).Update(submodules, true, true, false); + } + + if (needPopStash) + new Commands.Stash(_repo.FullPath).Use(log).Pop("stash@{0}"); + } log.Complete(); @@ -73,7 +100,7 @@ public override Task Sure() }); Task.Delay(400).Wait(); - return rs; + return succ; }); } diff --git a/src/ViewModels/CheckoutCommit.cs b/src/ViewModels/CheckoutCommit.cs index 7bd6976de..a8c5b8cfe 100644 --- a/src/ViewModels/CheckoutCommit.cs +++ b/src/ViewModels/CheckoutCommit.cs @@ -15,11 +15,24 @@ public bool DiscardLocalChanges set; } + public bool IsRecurseSubmoduleVisible + { + get; + private set; + } + + public bool RecurseSubmodules + { + get => _repo.Settings.UpdateSubmodulesOnCheckoutBranch; + set => _repo.Settings.UpdateSubmodulesOnCheckoutBranch = value; + } + public CheckoutCommit(Repository repo, Models.Commit commit) { _repo = repo; Commit = commit; DiscardLocalChanges = false; + IsRecurseSubmoduleVisible = repo.Submodules.Count > 0; } public override Task Sure() @@ -30,19 +43,22 @@ public override Task Sure() var log = _repo.CreateLog("Checkout Commit"); Use(log); + var updateSubmodules = IsRecurseSubmoduleVisible && RecurseSubmodules; return Task.Run(() => { - var changes = new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).Result(); - var needPopStash = false; - if (changes > 0) + var succ = false; + var needPop = false; + + if (DiscardLocalChanges) { - if (DiscardLocalChanges) - { - Commands.Discard.All(_repo.FullPath, false, log); - } - else + succ = new Commands.Checkout(_repo.FullPath).Use(log).Commit(Commit.SHA, true); + } + else + { + var changes = new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).Result(); + if (changes > 0) { - var succ = new Commands.Stash(_repo.FullPath).Use(log).Push("CHECKOUT_AUTO_STASH"); + succ = new Commands.Stash(_repo.FullPath).Use(log).Push("CHECKOUT_AUTO_STASH"); if (!succ) { log.Complete(); @@ -50,17 +66,28 @@ public override Task Sure() return false; } - needPopStash = true; + needPop = true; } + + succ = new Commands.Checkout(_repo.FullPath).Use(log).Commit(Commit.SHA, false); } - var rs = new Commands.Checkout(_repo.FullPath).Use(log).Commit(Commit.SHA); - if (needPopStash) - rs = new Commands.Stash(_repo.FullPath).Use(log).Pop("stash@{0}"); + if (succ) + { + if (updateSubmodules) + { + var submodules = new Commands.QuerySubmodules(_repo.FullPath).Result(); + if (submodules.Count > 0) + new Commands.Submodule(_repo.FullPath).Use(log).Update(submodules, true, true, false); + } + + if (needPop) + new Commands.Stash(_repo.FullPath).Use(log).Pop("stash@{0}"); + } log.Complete(); CallUIThread(() => _repo.SetWatcherEnabled(true)); - return rs; + return succ; }); } diff --git a/src/ViewModels/Clone.cs b/src/ViewModels/Clone.cs index ee5da69e9..dd9cf86b2 100644 --- a/src/ViewModels/Clone.cs +++ b/src/ViewModels/Clone.cs @@ -101,7 +101,6 @@ public override Task Sure() { ProgressDescription = "Clone ..."; - // Create a temp log. var log = new CommandLog("Clone"); Use(log); @@ -139,12 +138,11 @@ public override Task Sure() 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) - new Commands.Submodule(path).Use(log).Update(submodule.Path, true, true, false); + var submodules = new Commands.QuerySubmodules(path).Result(); + if (submodules.Count > 0) + new Commands.Submodule(path).Use(log).Update(submodules, true, true, false); } log.Complete(); diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index fdaa29f12..3b411056a 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -291,7 +291,7 @@ public ContextMenu CreateChangeContextMenu(Models.Change change) ev.Handled = true; }; - var fullPath = Path.Combine(_repo.FullPath, change.Path); + var fullPath = Native.OS.GetAbsPath(_repo.FullPath, change.Path); var explore = new MenuItem(); explore.Header = App.Text("RevealFile"); explore.Icon = App.CreateMenuIcon("Icons.Explore"); @@ -362,11 +362,9 @@ public ContextMenu CreateChangeContextMenu(Models.Change change) var resetToThisRevision = new MenuItem(); resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision"); resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout"); - resetToThisRevision.Click += (_, ev) => + resetToThisRevision.Click += async (_, ev) => { - var log = _repo.CreateLog($"Reset File to '{_commit.SHA}'"); - new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevision(change.Path, $"{_commit.SHA}"); - log.Complete(); + await ResetToThisRevision(change.Path); ev.Handled = true; }; @@ -374,14 +372,9 @@ public ContextMenu CreateChangeContextMenu(Models.Change change) resetToFirstParent.Header = App.Text("ChangeCM.CheckoutFirstParentRevision"); resetToFirstParent.Icon = App.CreateMenuIcon("Icons.File.Checkout"); resetToFirstParent.IsEnabled = _commit.Parents.Count > 0; - resetToFirstParent.Click += (_, ev) => + resetToFirstParent.Click += async (_, ev) => { - var log = _repo.CreateLog($"Reset File to '{_commit.SHA}~1'"); - if (change.Index == Models.ChangeState.Renamed) - new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevision(change.OriginalPath, $"{_commit.SHA}~1"); - - new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevision(change.Path, $"{_commit.SHA}~1"); - log.Complete(); + await ResetToParentRevision(change); ev.Handled = true; }; @@ -389,8 +382,7 @@ public ContextMenu CreateChangeContextMenu(Models.Change change) menu.Items.Add(resetToFirstParent); menu.Items.Add(new MenuItem { Header = "-" }); - if (File.Exists(Path.Combine(fullPath))) - TryToAddContextMenuItemsForGitLFS(menu, change.Path); + TryToAddContextMenuItemsForGitLFS(menu, fullPath, change.Path); } var copyPath = new MenuItem(); @@ -407,7 +399,7 @@ public ContextMenu CreateChangeContextMenu(Models.Change change) copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); copyFullPath.Click += (_, e) => { - App.CopyText(Native.OS.GetAbsPath(_repo.FullPath, change.Path)); + App.CopyText(fullPath); e.Handled = true; }; @@ -419,7 +411,7 @@ public ContextMenu CreateChangeContextMenu(Models.Change change) public ContextMenu CreateRevisionFileContextMenu(Models.Object file) { var menu = new ContextMenu(); - var fullPath = Path.Combine(_repo.FullPath, file.Path); + var fullPath = Native.OS.GetAbsPath(_repo.FullPath, file.Path); var explore = new MenuItem(); explore.Header = App.Text("RevealFile"); explore.Icon = App.CreateMenuIcon("Icons.Explore"); @@ -499,38 +491,34 @@ public ContextMenu CreateRevisionFileContextMenu(Models.Object file) menu.Items.Add(blame); menu.Items.Add(new MenuItem() { Header = "-" }); - var resetToThisRevision = new MenuItem(); - resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision"); - resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout"); - resetToThisRevision.IsEnabled = File.Exists(fullPath); - resetToThisRevision.Click += (_, ev) => + if (!_repo.IsBare) { - var log = _repo.CreateLog($"Reset File to '{_commit.SHA}'"); - new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevision(file.Path, $"{_commit.SHA}"); - log.Complete(); - ev.Handled = true; - }; + var resetToThisRevision = new MenuItem(); + resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision"); + resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToThisRevision.Click += async (_, ev) => + { + await ResetToThisRevision(file.Path); + ev.Handled = true; + }; - var resetToFirstParent = new MenuItem(); - resetToFirstParent.Header = App.Text("ChangeCM.CheckoutFirstParentRevision"); - resetToFirstParent.Icon = App.CreateMenuIcon("Icons.File.Checkout"); - var fileInChanges = _changes.Find(x => x.Path == file.Path); - var fileIndex = fileInChanges?.Index; - resetToFirstParent.IsEnabled = _commit.Parents.Count > 0 && fileIndex != Models.ChangeState.Renamed; - resetToFirstParent.Click += (_, ev) => - { - var log = _repo.CreateLog($"Reset File to '{_commit.SHA}~1'"); - new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevision(file.Path, $"{_commit.SHA}~1"); - log.Complete(); - ev.Handled = true; - }; + var change = _changes.Find(x => x.Path == file.Path) ?? new Models.Change() { Index = Models.ChangeState.None, Path = file.Path }; + var resetToFirstParent = new MenuItem(); + resetToFirstParent.Header = App.Text("ChangeCM.CheckoutFirstParentRevision"); + resetToFirstParent.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToFirstParent.IsEnabled = _commit.Parents.Count > 0; + resetToFirstParent.Click += async (_, ev) => + { + await ResetToParentRevision(change); + ev.Handled = true; + }; - menu.Items.Add(resetToThisRevision); - menu.Items.Add(resetToFirstParent); - menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(resetToThisRevision); + menu.Items.Add(resetToFirstParent); + menu.Items.Add(new MenuItem() { Header = "-" }); - if (File.Exists(Path.Combine(fullPath))) - TryToAddContextMenuItemsForGitLFS(menu, file.Path); + TryToAddContextMenuItemsForGitLFS(menu, fullPath, file.Path); + } var copyPath = new MenuItem(); copyPath.Header = App.Text("CopyPath"); @@ -546,7 +534,7 @@ public ContextMenu CreateRevisionFileContextMenu(Models.Object file) copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); copyFullPath.Click += (_, e) => { - App.CopyText(Native.OS.GetAbsPath(_repo.FullPath, file.Path)); + App.CopyText(fullPath); e.Handled = true; }; @@ -725,8 +713,11 @@ private void RefreshVisibleChanges() } } - private void TryToAddContextMenuItemsForGitLFS(ContextMenu menu, string path) + private void TryToAddContextMenuItemsForGitLFS(ContextMenu menu, string fullPath, string path) { + if (_repo.Remotes.Count == 0 || !File.Exists(fullPath)) + return; + var lfsEnabled = new Commands.LFS(_repo.FullPath).IsEnabled(); if (!lfsEnabled) return; @@ -738,7 +729,6 @@ private void TryToAddContextMenuItemsForGitLFS(ContextMenu menu, string path) var lfsLock = new MenuItem(); lfsLock.Header = App.Text("GitLFS.Locks.Lock"); lfsLock.Icon = App.CreateMenuIcon("Icons.Lock"); - lfsLock.IsEnabled = _repo.Remotes.Count > 0; if (_repo.Remotes.Count == 1) { lfsLock.Click += async (_, e) => @@ -777,7 +767,6 @@ private void TryToAddContextMenuItemsForGitLFS(ContextMenu menu, string path) var lfsUnlock = new MenuItem(); lfsUnlock.Header = App.Text("GitLFS.Locks.Unlock"); lfsUnlock.Icon = App.CreateMenuIcon("Icons.Unlock"); - lfsUnlock.IsEnabled = _repo.Remotes.Count > 0; if (_repo.Remotes.Count == 1) { lfsUnlock.Click += async (_, e) => @@ -867,6 +856,31 @@ private void CalcRevisionFileSearchSuggestion() RevisionFileSearchSuggestion = suggestion; } + private Task ResetToThisRevision(string path) + { + var log = _repo.CreateLog($"Reset File to '{_commit.SHA}'"); + + return Task.Run(() => + { + new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevision(path, $"{_commit.SHA}"); + log.Complete(); + }); + } + + private Task ResetToParentRevision(Models.Change change) + { + var log = _repo.CreateLog($"Reset File to '{_commit.SHA}~1'"); + + return Task.Run(() => + { + if (change.Index == Models.ChangeState.Renamed) + new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevision(change.OriginalPath, $"{_commit.SHA}~1"); + + new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevision(change.Path, $"{_commit.SHA}~1"); + log.Complete(); + }); + } + [GeneratedRegex(@"\b(https?://|ftp://)[\w\d\._/\-~%@()+:?&=#!]*[\w\d/]")] private static partial Regex REG_URL_FORMAT(); diff --git a/src/ViewModels/CreateBranch.cs b/src/ViewModels/CreateBranch.cs index dd190e18b..252802f43 100644 --- a/src/ViewModels/CreateBranch.cs +++ b/src/ViewModels/CreateBranch.cs @@ -28,7 +28,14 @@ public bool DiscardLocalChanges public bool CheckoutAfterCreated { get => _repo.Settings.CheckoutBranchOnCreateBranch; - set => _repo.Settings.CheckoutBranchOnCreateBranch = value; + set + { + if (_repo.Settings.CheckoutBranchOnCreateBranch != value) + { + _repo.Settings.CheckoutBranchOnCreateBranch = value; + OnPropertyChanged(); + } + } } public bool IsBareRepository @@ -36,6 +43,18 @@ public bool IsBareRepository get => _repo.IsBare; } + public bool IsRecurseSubmoduleVisible + { + get; + private set; + } + + public bool RecurseSubmodules + { + get => _repo.Settings.UpdateSubmodulesOnCheckoutBranch; + set => _repo.Settings.UpdateSubmodulesOnCheckoutBranch = value; + } + public CreateBranch(Repository repo, Models.Branch branch) { _repo = repo; @@ -48,6 +67,7 @@ public CreateBranch(Repository repo, Models.Branch branch) BasedOn = branch; DiscardLocalChanges = false; + IsRecurseSubmoduleVisible = repo.Submodules.Count > 0; } public CreateBranch(Repository repo, Models.Commit commit) @@ -57,6 +77,7 @@ public CreateBranch(Repository repo, Models.Commit commit) BasedOn = commit; DiscardLocalChanges = false; + IsRecurseSubmoduleVisible = repo.Submodules.Count > 0; } public CreateBranch(Repository repo, Models.Tag tag) @@ -66,6 +87,7 @@ public CreateBranch(Repository repo, Models.Tag tag) BasedOn = tag; DiscardLocalChanges = false; + IsRecurseSubmoduleVisible = repo.Submodules.Count > 0; } public static ValidationResult ValidateBranchName(string name, ValidationContext ctx) @@ -92,20 +114,21 @@ public override Task Sure() var log = _repo.CreateLog($"Create Branch '{fixedName}'"); Use(log); + var updateSubmodules = IsRecurseSubmoduleVisible && RecurseSubmodules; return Task.Run(() => { - bool succ; + bool succ = false; if (CheckoutAfterCreated && !_repo.IsBare) { - var changes = new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).Result(); var needPopStash = false; - if (changes > 0) + if (DiscardLocalChanges) { - if (DiscardLocalChanges) - { - Commands.Discard.All(_repo.FullPath, false, log); - } - else + succ = new Commands.Checkout(_repo.FullPath).Use(log).Branch(fixedName, _baseOnRevision, true); + } + else + { + var changes = new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).Result(); + if (changes > 0) { succ = new Commands.Stash(_repo.FullPath).Use(log).Push("CREATE_BRANCH_AUTO_STASH"); if (!succ) @@ -117,11 +140,22 @@ public override Task Sure() needPopStash = true; } + + succ = new Commands.Checkout(_repo.FullPath).Use(log).Branch(fixedName, _baseOnRevision, false); } - succ = new Commands.Checkout(_repo.FullPath).Use(log).Branch(fixedName, _baseOnRevision); - if (needPopStash) - new Commands.Stash(_repo.FullPath).Use(log).Pop("stash@{0}"); + if (succ) + { + if (updateSubmodules) + { + var submodules = new Commands.QuerySubmodules(_repo.FullPath).Result(); + if (submodules.Count > 0) + new Commands.Submodule(_repo.FullPath).Use(log).Update(submodules, true, true, false); + } + + if (needPopStash) + new Commands.Stash(_repo.FullPath).Use(log).Pop("stash@{0}"); + } } else { @@ -144,6 +178,8 @@ public override Task Sure() if (_repo.HistoriesFilterMode == Models.FilterMode.Included) _repo.SetBranchFilterMode(fake, Models.FilterMode.Included, true, false); + + ProgressDescription = "Waiting for branch updated..."; } _repo.MarkBranchesDirtyManually(); @@ -151,10 +187,7 @@ public override Task Sure() }); if (CheckoutAfterCreated) - { - CallUIThread(() => ProgressDescription = "Waiting for branch updated..."); Task.Delay(400).Wait(); - } return true; }); diff --git a/src/ViewModels/GitFlowFinish.cs b/src/ViewModels/GitFlowFinish.cs index efc28db16..d7ad5d315 100644 --- a/src/ViewModels/GitFlowFinish.cs +++ b/src/ViewModels/GitFlowFinish.cs @@ -13,6 +13,18 @@ public Models.Branch Branch public bool IsRelease => _type == "release"; public bool IsHotfix => _type == "hotfix"; + public bool Squash + { + get; + set; + } = false; + + public bool AutoPush + { + get; + set; + } = false; + public bool KeepBranch { get; @@ -39,7 +51,7 @@ public override Task Sure() return Task.Run(() => { - var succ = Commands.GitFlow.Finish(_repo.FullPath, _type, name, KeepBranch, log); + var succ = Commands.GitFlow.Finish(_repo.FullPath, _type, name, Squash, AutoPush, KeepBranch, log); log.Complete(); CallUIThread(() => _repo.SetWatcherEnabled(true)); return succ; diff --git a/src/ViewModels/LauncherPage.cs b/src/ViewModels/LauncherPage.cs index b97ef6f71..8a59d2460 100644 --- a/src/ViewModels/LauncherPage.cs +++ b/src/ViewModels/LauncherPage.cs @@ -100,23 +100,32 @@ public void StartPopup(Popup popup) public async void ProcessPopup() { - if (_popup is { InProgress: false }) + if (_popup is { InProgress: false } dump) { - if (!_popup.Check()) + if (!dump.Check()) return; - _popup.InProgress = true; - var task = _popup.Sure(); + dump.InProgress = true; + var task = dump.Sure(); + var finished = false; if (task != null) { - var finished = await task; - _popup.InProgress = false; + try + { + finished = await task; + } + catch (Exception e) + { + App.LogException(e); + } + + dump.InProgress = false; if (finished) Popup = null; } else { - _popup.InProgress = false; + dump.InProgress = false; Popup = null; } } diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 289d890f3..8661cae26 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -583,43 +583,85 @@ public void RefreshAll() Task.Run(RefreshStashes); } - public void OpenInFileManager() + public ContextMenu CreateContextMenuForExternalTools() { - Native.OS.OpenInFileManager(_fullpath); - } + var menu = new ContextMenu(); + menu.Placement = PlacementMode.BottomEdgeAlignedLeft; - public void OpenInTerminal() - { - Native.OS.OpenTerminal(_fullpath); - } + RenderOptions.SetBitmapInterpolationMode(menu, BitmapInterpolationMode.HighQuality); + RenderOptions.SetEdgeMode(menu, EdgeMode.Antialias); + RenderOptions.SetTextRenderingMode(menu, TextRenderingMode.Antialias); + + var explore = new MenuItem(); + explore.Header = App.Text("Repository.Explore"); + explore.Icon = App.CreateMenuIcon("Icons.Explore"); + explore.Click += (_, e) => + { + Native.OS.OpenInFileManager(_fullpath); + e.Handled = true; + }; + + var terminal = new MenuItem(); + terminal.Header = App.Text("Repository.Terminal"); + terminal.Icon = App.CreateMenuIcon("Icons.Terminal"); + terminal.Click += (_, e) => + { + Native.OS.OpenTerminal(_fullpath); + e.Handled = true; + }; + + menu.Items.Add(explore); + menu.Items.Add(terminal); - public ContextMenu CreateContextMenuForExternalTools() - { var tools = Native.OS.ExternalTools; - if (tools.Count == 0) + if (tools.Count > 0) { - App.RaiseException(_fullpath, "No available external editors found!"); - return null; + menu.Items.Add(new MenuItem() { Header = "-" }); + + foreach (var tool in Native.OS.ExternalTools) + { + var dupTool = tool; + + var item = new MenuItem(); + item.Header = App.Text("Repository.OpenIn", dupTool.Name); + item.Icon = new Image { Width = 16, Height = 16, Source = dupTool.IconImage }; + item.Click += (_, e) => + { + dupTool.Open(_fullpath); + e.Handled = true; + }; + + menu.Items.Add(item); + } } - var menu = new ContextMenu(); - menu.Placement = PlacementMode.BottomEdgeAlignedLeft; - RenderOptions.SetBitmapInterpolationMode(menu, BitmapInterpolationMode.HighQuality); + var urls = new Dictionary(); + foreach (var r in _remotes) + { + if (r.TryGetVisitURL(out var visit)) + urls.Add(r.Name, visit); + } - foreach (var tool in tools) + if (urls.Count > 0) { - var dupTool = tool; + menu.Items.Add(new MenuItem() { Header = "-" }); - var item = new MenuItem(); - item.Header = App.Text("Repository.OpenIn", dupTool.Name); - item.Icon = new Image { Width = 16, Height = 16, Source = dupTool.IconImage }; - item.Click += (_, e) => + foreach (var url in urls) { - dupTool.Open(_fullpath); - e.Handled = true; - }; + var name = url.Key; + var addr = url.Value; + + var item = new MenuItem(); + item.Header = App.Text("Repository.Visit", name); + item.Icon = App.CreateMenuIcon("Icons.Remotes"); + item.Click += (_, e) => + { + Native.OS.OpenBrowser(addr); + e.Handled = true; + }; - menu.Items.Add(item); + menu.Items.Add(item); + } } return menu; @@ -1154,7 +1196,7 @@ public void CheckoutBranch(Models.Branch branch) if (branch.IsLocal) { - if (_localChangesCount > 0) + if (_localChangesCount > 0 || _submodules.Count > 0) ShowPopup(new Checkout(this, branch.Name)); else ShowAndStartPopup(new Checkout(this, branch.Name)); @@ -2187,6 +2229,51 @@ public ContextMenu CreateContextMenuForTag(Models.Tag tag) return menu; } + public ContextMenu CreateContextMenuForBranchSortMode(bool local) + { + var mode = local ? _settings.LocalBranchSortMode : _settings.RemoteBranchSortMode; + var changeMode = new Action(m => + { + if (local) + _settings.LocalBranchSortMode = m; + else + _settings.RemoteBranchSortMode = m; + + var builder = BuildBranchTree(_branches, _remotes); + LocalBranchTrees = builder.Locals; + RemoteBranchTrees = builder.Remotes; + }); + + var byNameAsc = new MenuItem(); + byNameAsc.Header = App.Text("Repository.BranchSort.ByName"); + if (mode == Models.BranchSortMode.Name) + byNameAsc.Icon = App.CreateMenuIcon("Icons.Check"); + byNameAsc.Click += (_, ev) => + { + if (mode != Models.BranchSortMode.Name) + changeMode(Models.BranchSortMode.Name); + + ev.Handled = true; + }; + + var byCommitterDate = new MenuItem(); + byCommitterDate.Header = App.Text("Repository.BranchSort.ByCommitterDate"); + if (mode == Models.BranchSortMode.CommitterDate) + byCommitterDate.Icon = App.CreateMenuIcon("Icons.Check"); + byCommitterDate.Click += (_, ev) => + { + if (mode != Models.BranchSortMode.CommitterDate) + changeMode(Models.BranchSortMode.CommitterDate); + + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(byNameAsc); + menu.Items.Add(byCommitterDate); + return menu; + } + public ContextMenu CreateContextMenuForTagSortMode() { var mode = _settings.TagSortMode; @@ -2356,7 +2443,7 @@ private LauncherPage GetOwnerPage() private BranchTreeNode.Builder BuildBranchTree(List branches, List remotes) { - var builder = new BranchTreeNode.Builder(); + var builder = new BranchTreeNode.Builder(_settings.LocalBranchSortMode, _settings.RemoteBranchSortMode); if (string.IsNullOrEmpty(_filter)) { builder.SetExpandedNodes(_settings.ExpandedBranchNodesInSideBar); diff --git a/src/Views/BranchCompare.axaml b/src/Views/BranchCompare.axaml index 4f6f312e6..1fe66e9b9 100644 --- a/src/Views/BranchCompare.axaml +++ b/src/Views/BranchCompare.axaml @@ -46,7 +46,7 @@ - + - + + + Content="{DynamicResource Text.Checkout.LocalChanges.Discard}"/> + + diff --git a/src/Views/CheckoutCommit.axaml b/src/Views/CheckoutCommit.axaml index 9b4188231..11b4b5d04 100644 --- a/src/Views/CheckoutCommit.axaml +++ b/src/Views/CheckoutCommit.axaml @@ -12,7 +12,7 @@ Classes="bold" Text="{DynamicResource Text.Checkout.Commit}" /> - + - + + + diff --git a/src/Views/CommitGraph.cs b/src/Views/CommitGraph.cs index 015eaca5f..5db393009 100644 --- a/src/Views/CommitGraph.cs +++ b/src/Views/CommitGraph.cs @@ -55,23 +55,29 @@ public override void Render(DrawingContext context) if (list == null) return; - // Calculate drawing area. - double width = Bounds.Width - 273 - histories.AuthorNameColumnWidth.Value; - double height = Bounds.Height; - double startY = list.Scroll?.Offset.Y ?? 0; - double endY = startY + height + 28; + var container = list.ItemsPanelRoot as VirtualizingStackPanel; + if (container == null) + return; + + var item = list.ContainerFromIndex(container.FirstRealizedIndex); + if (item == null) + return; + + var width = histories.CommitListHeader.ColumnDefinitions[0].ActualWidth; + var height = Bounds.Height; + var rowHeight = item.Bounds.Height; + var startY = container.FirstRealizedIndex * rowHeight - item.TranslatePoint(new Point(0, 0), list).Value!.Y; + var endY = startY + height + 28; - // Apply scroll offset and clip. using (context.PushClip(new Rect(0, 0, width, height))) using (context.PushTransform(Matrix.CreateTranslation(0, -startY))) { - // Draw contents - DrawCurves(context, graph, startY, endY); - DrawAnchors(context, graph, startY, endY); + DrawCurves(context, graph, startY, endY, rowHeight); + DrawAnchors(context, graph, startY, endY, rowHeight); } } - private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double top, double bottom) + private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double top, double bottom, double rowHeight) { var grayedPen = new Pen(new SolidColorBrush(Colors.Gray, 0.4), Models.CommitGraph.Pens[0].Thickness); var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch; @@ -82,16 +88,20 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double { if (link.IsMerged) continue; - if (link.End.Y < top) + + var startY = link.Start.Y * rowHeight; + var endY = link.End.Y * rowHeight; + + if (endY < top) continue; - if (link.Start.Y > bottom) + if (startY > bottom) break; var geo = new StreamGeometry(); using (var ctx = geo.Open()) { - ctx.BeginFigure(link.Start, false); - ctx.QuadraticBezierTo(link.Control, link.End); + ctx.BeginFigure(new Point(link.Start.X, startY), false); + ctx.QuadraticBezierTo(new Point(link.Control.X, link.Control.Y * rowHeight), new Point(link.End.X, endY)); } context.DrawGeometry(null, grayedPen, geo); @@ -100,10 +110,11 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double foreach (var line in graph.Paths) { - var last = line.Points[0]; + var last = new Point(line.Points[0].X, line.Points[0].Y * rowHeight); var size = line.Points.Count; + var endY = line.Points[size - 1].Y * rowHeight; - if (line.Points[size - 1].Y < top) + if (endY < top) continue; if (last.Y > bottom) break; @@ -117,7 +128,7 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double var ended = false; for (int i = 1; i < size; i++) { - var cur = line.Points[i]; + var cur = new Point(line.Points[i].X, line.Points[i].Y * rowHeight); if (cur.Y < top) { last = cur; @@ -173,23 +184,27 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double { if (onlyHighlightCurrentBranch && !link.IsMerged) continue; - if (link.End.Y < top) + + var startY = link.Start.Y * rowHeight; + var endY = link.End.Y * rowHeight; + + if (endY < top) continue; - if (link.Start.Y > bottom) + if (startY > bottom) break; var geo = new StreamGeometry(); using (var ctx = geo.Open()) { - ctx.BeginFigure(link.Start, false); - ctx.QuadraticBezierTo(link.Control, link.End); + ctx.BeginFigure(new Point(link.Start.X, startY), false); + ctx.QuadraticBezierTo(new Point(link.Control.X, link.Control.Y * rowHeight), new Point(link.End.X, endY)); } context.DrawGeometry(null, Models.CommitGraph.Pens[link.Color], geo); } } - private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, double top, double bottom) + private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, double top, double bottom, double rowHeight) { var dotFill = DotBrush; var dotFillPen = new Pen(dotFill, 2); @@ -198,9 +213,11 @@ private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, doubl foreach (var dot in graph.Dots) { - if (dot.Center.Y < top) + var center = new Point(dot.Center.X, dot.Center.Y * rowHeight); + + if (center.Y < top) continue; - if (dot.Center.Y > bottom) + if (center.Y > bottom) break; var pen = Models.CommitGraph.Pens[dot.Color]; @@ -210,16 +227,16 @@ private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, doubl switch (dot.Type) { case Models.CommitGraph.DotType.Head: - context.DrawEllipse(dotFill, pen, dot.Center, 6, 6); - context.DrawEllipse(pen.Brush, null, dot.Center, 3, 3); + context.DrawEllipse(dotFill, pen, center, 6, 6); + context.DrawEllipse(pen.Brush, null, center, 3, 3); break; case Models.CommitGraph.DotType.Merge: - context.DrawEllipse(pen.Brush, null, dot.Center, 6, 6); - context.DrawLine(dotFillPen, new Point(dot.Center.X, dot.Center.Y - 3), new Point(dot.Center.X, dot.Center.Y + 3)); - context.DrawLine(dotFillPen, new Point(dot.Center.X - 3, dot.Center.Y), new Point(dot.Center.X + 3, dot.Center.Y)); + context.DrawEllipse(pen.Brush, null, center, 6, 6); + context.DrawLine(dotFillPen, new Point(center.X, center.Y - 3), new Point(center.X, center.Y + 3)); + context.DrawLine(dotFillPen, new Point(center.X - 3, center.Y), new Point(center.X + 3, center.Y)); break; default: - context.DrawEllipse(dotFill, pen, dot.Center, 3, 3); + context.DrawEllipse(dotFill, pen, center, 3, 3); break; } } diff --git a/src/Views/CommitSubjectPresenter.cs b/src/Views/CommitSubjectPresenter.cs index 86e0fd54b..18902462f 100644 --- a/src/Views/CommitSubjectPresenter.cs +++ b/src/Views/CommitSubjectPresenter.cs @@ -104,23 +104,32 @@ public override void Render(DrawingContext context) if (_inlines.Count == 0) return; - var height = Bounds.Height; - var width = Bounds.Width; - foreach (var inline in _inlines) + var ro = new RenderOptions() { - if (inline.X > width) - return; + TextRenderingMode = TextRenderingMode.SubpixelAntialias, + EdgeMode = EdgeMode.Antialias + }; - if (inline.Element is { Type: Models.InlineElementType.Code }) - { - var rect = new Rect(inline.X, (height - inline.Text.Height - 2) * 0.5, inline.Text.WidthIncludingTrailingWhitespace + 8, inline.Text.Height + 2); - var roundedRect = new RoundedRect(rect, new CornerRadius(4)); - context.DrawRectangle(InlineCodeBackground, null, roundedRect); - context.DrawText(inline.Text, new Point(inline.X + 4, (height - inline.Text.Height) * 0.5)); - } - else + using (context.PushRenderOptions(ro)) + { + var height = Bounds.Height; + var width = Bounds.Width; + foreach (var inline in _inlines) { - context.DrawText(inline.Text, new Point(inline.X, (height - inline.Text.Height) * 0.5)); + if (inline.X > width) + return; + + if (inline.Element is { Type: Models.InlineElementType.Code }) + { + var rect = new Rect(inline.X, (height - inline.Text.Height - 2) * 0.5, inline.Text.WidthIncludingTrailingWhitespace + 8, inline.Text.Height + 2); + var roundedRect = new RoundedRect(rect, new CornerRadius(4)); + context.DrawRectangle(InlineCodeBackground, null, roundedRect); + context.DrawText(inline.Text, new Point(inline.X + 4, (height - inline.Text.Height) * 0.5)); + } + else + { + context.DrawText(inline.Text, new Point(inline.X, (height - inline.Text.Height) * 0.5)); + } } } } @@ -299,7 +308,7 @@ private void GenerateFormattedTextElements() CultureInfo.CurrentCulture, FlowDirection.LeftToRight, codeTypeface, - fontSize, + fontSize - 0.5, foreground); _inlines.Add(new Inline(x, link, elem)); x += link.WidthIncludingTrailingWhitespace + 8; diff --git a/src/Views/Conflict.axaml b/src/Views/Conflict.axaml index cf2e25a07..6e8aceb3f 100644 --- a/src/Views/Conflict.axaml +++ b/src/Views/Conflict.axaml @@ -52,7 +52,7 @@ - + + + + + + + + + + + diff --git a/src/Views/GitFlowFinish.axaml b/src/Views/GitFlowFinish.axaml index 7af46fd98..6a16e95cd 100644 --- a/src/Views/GitFlowFinish.axaml +++ b/src/Views/GitFlowFinish.axaml @@ -19,7 +19,7 @@ Classes="bold" Text="{DynamicResource Text.GitFlow.FinishHotfix}" IsVisible="{Binding IsHotfix}"/> - + + + + + + IsChecked="{Binding KeepBranch, Mode=TwoWay}" + ToolTip.Tip="-k"/> diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index 86a965289..c9a8ba5e3 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -30,7 +30,7 @@ Background="{DynamicResource Brush.Window}" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}"> - + @@ -76,7 +76,7 @@