diff --git a/README.md b/README.md index 50f0be5d..0af00613 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Opensource Git GUI client. * GIT commands with GUI * Clone/Fetch/Pull/Push... * Merge/Rebase/Reset/Revert/Amend/Cherry-pick... + * Amend/Reword * Interactive rebase (Basic) * Branches * Remotes @@ -30,8 +31,9 @@ Opensource Git GUI client. * Revision Diffs * Branch Diff * Image Diff - Side-By-Side/Swipe/Blend -* GitFlow support -* Git LFS support +* GitFlow +* Git LFS +* Issue Link > **Linux** only tested on **Debian 12** on both **X11** & **Wayland**. diff --git a/VERSION b/VERSION index 86a3e56a..68697155 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.24 \ No newline at end of file +8.25 \ No newline at end of file diff --git a/src/Commands/CountLocalChangesWithoutUntracked.cs b/src/Commands/CountLocalChangesWithoutUntracked.cs new file mode 100644 index 00000000..afb62840 --- /dev/null +++ b/src/Commands/CountLocalChangesWithoutUntracked.cs @@ -0,0 +1,26 @@ +using System; + +namespace SourceGit.Commands +{ + public class CountLocalChangesWithoutUntracked : Command + { + public CountLocalChangesWithoutUntracked(string repo) + { + WorkingDirectory = repo; + Context = repo; + Args = "status -uno --ignore-submodules=dirty --porcelain"; + } + + public int Result() + { + var rs = ReadToEnd(); + if (rs.IsSuccess) + { + var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries); + return lines.Length; + } + + return 0; + } + } +} diff --git a/src/Commands/QueryBranches.cs b/src/Commands/QueryBranches.cs index e598ee08..5f9d31a8 100644 --- a/src/Commands/QueryBranches.cs +++ b/src/Commands/QueryBranches.cs @@ -7,7 +7,8 @@ namespace SourceGit.Commands { private const string PREFIX_LOCAL = "refs/heads/"; private const string PREFIX_REMOTE = "refs/remotes/"; - private const string PREFIX_DETACHED = "(HEAD detached at"; + private const string PREFIX_DETACHED_AT = "(HEAD detached at"; + private const string PREFIX_DETACHED_FROM = "(HEAD detached from"; public QueryBranches(string repo) { @@ -37,9 +38,9 @@ namespace SourceGit.Commands if (refName.EndsWith("/HEAD", StringComparison.Ordinal)) return; - if (refName.StartsWith(PREFIX_DETACHED, StringComparison.Ordinal)) + if (refName.StartsWith(PREFIX_DETACHED_AT, StringComparison.Ordinal) || refName.StartsWith(PREFIX_DETACHED_FROM, StringComparison.Ordinal)) { - branch.IsHead = true; + branch.IsDetachedHead = true; } if (refName.StartsWith(PREFIX_LOCAL, StringComparison.Ordinal)) diff --git a/src/Commands/QuerySubmodules.cs b/src/Commands/QuerySubmodules.cs index 622de2fc..5fd6e3d5 100644 --- a/src/Commands/QuerySubmodules.cs +++ b/src/Commands/QuerySubmodules.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text; using System.Text.RegularExpressions; namespace SourceGit.Commands @@ -9,6 +10,8 @@ namespace SourceGit.Commands private static partial Regex REG_FORMAT1(); [GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)$")] private static partial Regex REG_FORMAT2(); + [GeneratedRegex(@"^\s?[\w\?]{1,4}\s+(.+)$")] + private static partial Regex REG_FORMAT_STATUS(); public QuerySubmodules(string repo) { @@ -17,28 +20,59 @@ namespace SourceGit.Commands Args = "submodule status"; } - public List Result() + public List Result() { - Exec(); - return _submodules; - } + var submodules = new List(); + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return submodules; - protected override void OnReadline(string line) - { - var match = REG_FORMAT1().Match(line); - if (match.Success) + var builder = new StringBuilder(); + var lines = rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) { - _submodules.Add(match.Groups[1].Value); - return; + var match = REG_FORMAT1().Match(line); + if (match.Success) + { + var path = match.Groups[1].Value; + builder.Append($"\"{path}\" "); + submodules.Add(new Models.Submodule() { Path = path }); + continue; + } + + match = REG_FORMAT2().Match(line); + if (match.Success) + { + var path = match.Groups[1].Value; + builder.Append($"\"{path}\" "); + submodules.Add(new Models.Submodule() { Path = path }); + } } - match = REG_FORMAT2().Match(line); - if (match.Success) + if (submodules.Count > 0) { - _submodules.Add(match.Groups[1].Value); - } - } + Args = $"status -uno --porcelain -- {builder}"; + rs = ReadToEnd(); + if (!rs.IsSuccess) + return submodules; - private readonly List _submodules = new List(); + var dirty = new HashSet(); + lines = rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var match = REG_FORMAT_STATUS().Match(line); + if (match.Success) + { + var path = match.Groups[1].Value; + dirty.Add(path); + } + } + + foreach (var submodule in submodules) + submodule.IsDirty = dirty.Contains(submodule.Path); + } + + return submodules; + } } } diff --git a/src/Models/AvatarManager.cs b/src/Models/AvatarManager.cs index a380c1bc..e85de1fd 100644 --- a/src/Models/AvatarManager.cs +++ b/src/Models/AvatarManager.cs @@ -34,6 +34,9 @@ namespace SourceGit.Models if (!Directory.Exists(_storePath)) Directory.CreateDirectory(_storePath); + var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/github.png", UriKind.RelativeOrAbsolute)); + _resources.Add("noreply@github.com", new Bitmap(icon)); + Task.Run(() => { while (true) @@ -117,19 +120,11 @@ namespace SourceGit.Models public static Bitmap Request(string email, bool forceRefetch) { - if (email.Equals("noreply@github.com", StringComparison.Ordinal)) - { - if (_githubEmailAvatar == null) - { - var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/github.png", UriKind.RelativeOrAbsolute)); - _githubEmailAvatar = new Bitmap(icon); - } - - return _githubEmailAvatar; - } - if (forceRefetch) { + if (email.Equals("noreply@github.com", StringComparison.Ordinal)) + return null; + if (_resources.ContainsKey(email)) _resources.Remove(email); @@ -198,6 +193,5 @@ namespace SourceGit.Models [GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@users\.noreply\.github\.com$")] private static partial Regex REG_GITHUB_USER_EMAIL(); - private static Bitmap _githubEmailAvatar = null; } } diff --git a/src/Models/Branch.cs b/src/Models/Branch.cs index f6742d5b..ac6b8c67 100644 --- a/src/Models/Branch.cs +++ b/src/Models/Branch.cs @@ -28,10 +28,10 @@ namespace SourceGit.Models public string Head { get; set; } public bool IsLocal { get; set; } public bool IsCurrent { get; set; } + public bool IsDetachedHead { get; set; } public string Upstream { get; set; } public BranchTrackStatus TrackStatus { get; set; } public string Remote { get; set; } - public bool IsHead { get; set; } public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}"; } diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index f30596ae..6a199478 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -25,8 +25,6 @@ namespace SourceGit.Models public bool HasDecorators => Decorators.Count > 0; public bool IsMerged { get; set; } = false; - public bool CanPushToUpstream { get; set; } = false; - public bool CanPullFromUpstream { get; set; } = false; public Thickness Margin { get; set; } = new Thickness(0); public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss"); diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index c5c66482..6f371594 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -128,7 +128,7 @@ namespace SourceGit.Models _penCount = colors.Count; } - public static CommitGraph Parse(List commits, HashSet canPushCommits, HashSet canPullCommits) + public static CommitGraph Parse(List commits) { double UNIT_WIDTH = 12; double HALF_WIDTH = 6; @@ -148,9 +148,6 @@ namespace SourceGit.Models var isMerged = commit.IsMerged; var oldCount = unsolved.Count; - commit.CanPushToUpstream = canPushCommits.Remove(commit.SHA); - commit.CanPullFromUpstream = canPullCommits.Remove(commit.SHA); - // Update current y offset offsetY += UNIT_HEIGHT; diff --git a/src/Models/IssueTrackerRule.cs b/src/Models/IssueTrackerRule.cs new file mode 100644 index 00000000..127cfa98 --- /dev/null +++ b/src/Models/IssueTrackerRule.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.Models +{ + public class IssueTrackerMatch + { + public int Start { get; set; } = 0; + public int Length { get; set; } = 0; + public string URL { get; set; } = ""; + + public bool Intersect(int start, int length) + { + if (start == Start) + return true; + + if (start < Start) + return start + length > Start; + + return start < Start + Length; + } + } + + public class IssueTrackerRule : ObservableObject + { + public string Name + { + get => _name; + set => SetProperty(ref _name, value); + } + + public string RegexString + { + get => _regexString; + set + { + if (SetProperty(ref _regexString, value)) + { + try + { + _regex = null; + _regex = new Regex(_regexString, RegexOptions.Multiline); + } + catch + { + // Ignore errors. + } + } + + OnPropertyChanged(nameof(IsRegexValid)); + } + } + + public bool IsRegexValid + { + get => _regex != null; + } + + public string URLTemplate + { + get => _urlTemplate; + set => SetProperty(ref _urlTemplate, value); + } + + public void Matches(List outs, string message) + { + if (_regex == null || string.IsNullOrEmpty(_urlTemplate)) + return; + + var matches = _regex.Matches(message); + for (var i = 0; i < matches.Count; i++) + { + var match = matches[i]; + if (!match.Success) + continue; + + var start = match.Index; + var len = match.Length; + var intersect = false; + foreach (var exist in outs) + { + if (exist.Intersect(start, len)) + { + intersect = true; + break; + } + } + + if (intersect) + continue; + + var range = new IssueTrackerMatch(); + range.Start = start; + range.Length = len; + range.URL = _urlTemplate; + for (var j = 1; j < match.Groups.Count; j++) + { + var group = match.Groups[j]; + if (group.Success) + range.URL = range.URL.Replace($"${j}", group.Value); + } + + outs.Add(range); + } + } + + private string _name; + private string _regexString; + private string _urlTemplate; + private Regex _regex = null; + } +} diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs index 514a0d59..bf15c4f4 100644 --- a/src/Models/RepositorySettings.cs +++ b/src/Models/RepositorySettings.cs @@ -76,6 +76,12 @@ namespace SourceGit.Models set; } = new AvaloniaList(); + public AvaloniaList IssueTrackerRules + { + get; + set; + } = new AvaloniaList(); + public void PushCommitMessage(string message) { var existIdx = CommitMessages.IndexOf(message); @@ -93,5 +99,50 @@ namespace SourceGit.Models CommitMessages.Insert(0, message); } + + public IssueTrackerRule AddNewIssueTracker() + { + var rule = new IssueTrackerRule() + { + Name = "New Issue Tracker", + RegexString = "#(\\d+)", + URLTemplate = "https://xxx/$1", + }; + + IssueTrackerRules.Add(rule); + return rule; + } + + public IssueTrackerRule AddGithubIssueTracker(string repoURL) + { + var rule = new IssueTrackerRule() + { + Name = "Github ISSUE", + RegexString = "#(\\d+)", + URLTemplate = string.IsNullOrEmpty(repoURL) ? "https://github.com/username/repository/issues/$1" : $"{repoURL}/issues/$1", + }; + + IssueTrackerRules.Add(rule); + return rule; + } + + public IssueTrackerRule AddJiraIssueTracker() + { + var rule = new IssueTrackerRule() + { + Name = "Jira Tracker", + RegexString = "PROJ-(\\d+)", + URLTemplate = "https://jira.yourcompany.com/browse/PROJ-$1", + }; + + IssueTrackerRules.Add(rule); + return rule; + } + + public void RemoveIssueTracker(IssueTrackerRule rule) + { + if (rule != null) + IssueTrackerRules.Remove(rule); + } } } diff --git a/src/Models/RevisionFile.cs b/src/Models/RevisionFile.cs index e2f4abb0..59868fcc 100644 --- a/src/Models/RevisionFile.cs +++ b/src/Models/RevisionFile.cs @@ -14,6 +14,7 @@ namespace SourceGit.Models public class RevisionTextFile { + public string FileName { get; set; } public string Content { get; set; } } diff --git a/src/Models/Submodule.cs b/src/Models/Submodule.cs new file mode 100644 index 00000000..ce00ac02 --- /dev/null +++ b/src/Models/Submodule.cs @@ -0,0 +1,8 @@ +namespace SourceGit.Models +{ + public class Submodule + { + public string Path { get; set; } = ""; + public bool IsDirty { get; set; } = false; + } +} diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index f8aacecb..6cd77a15 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -70,6 +71,16 @@ namespace SourceGit.Models } } + public void SetSubmodules(List submodules) + { + lock (_lockSubmodule) + { + _submodules.Clear(); + foreach (var submodule in submodules) + _submodules.Add(submodule.Path); + } + } + public void MarkBranchDirtyManually() { _updateBranch = DateTime.Now.ToFileTime() - 1; @@ -168,9 +179,10 @@ namespace SourceGit.Models return; var name = e.Name.Replace("\\", "/"); - if (name.StartsWith("modules", StringComparison.Ordinal)) + if (name.StartsWith("modules", StringComparison.Ordinal) && name.EndsWith("HEAD", StringComparison.Ordinal)) { _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); + _updateWC = DateTime.Now.AddSeconds(1).ToFileTime(); } else if (name.StartsWith("refs/tags", StringComparison.Ordinal)) { @@ -186,6 +198,12 @@ namespace SourceGit.Models (name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal))) { _updateBranch = DateTime.Now.AddSeconds(.5).ToFileTime(); + + lock (_submodules) + { + if (_submodules.Count > 0) + _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); + } } else if (name.StartsWith("objects/", StringComparison.Ordinal) || name.Equals("index", StringComparison.Ordinal)) { @@ -201,6 +219,19 @@ namespace SourceGit.Models var name = e.Name.Replace("\\", "/"); if (name == ".git" || name.StartsWith(".git/", StringComparison.Ordinal)) return; + + lock (_submodules) + { + foreach (var submodule in _submodules) + { + if (name.StartsWith(submodule, StringComparison.Ordinal)) + { + _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); + return; + } + } + } + _updateWC = DateTime.Now.AddSeconds(1).ToFileTime(); } @@ -214,5 +245,8 @@ namespace SourceGit.Models private long _updateSubmodules = 0; private long _updateStashes = 0; private long _updateTags = 0; + + private object _lockSubmodule = new object(); + private List _submodules = new List(); } } diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index e2940f68..a159d764 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -52,6 +52,7 @@ M512 0C229 0 0 229 0 512s229 512 512 512 512-229 512-512S795 0 512 0zM512 928c-230 0-416-186-416-416S282 96 512 96s416 186 416 416S742 928 512 928zM538 343c47 0 83-38 83-78 0-32-21-61-62-61-55 0-82 45-82 77C475 320 498 343 538 343zM533 729c-8 0-11-10-3-40l43-166c16-61 11-100-22-100-39 0-131 40-211 108l16 27c25-17 68-35 78-35 8 0 7 10 0 36l-38 158c-23 89 1 110 34 110 33 0 118-30 196-110l-19-25C575 717 543 729 533 729z M412 66C326 132 271 233 271 347c0 17 1 34 4 50-41-48-98-79-162-83a444 444 0 00-46 196c0 207 142 382 337 439h2c19 0 34 15 34 33 0 11-6 21-14 26l1 14C183 973 0 763 0 511 0 272 166 70 393 7A35 35 0 01414 0c19 0 34 15 34 33a33 33 0 01-36 33zm200 893c86-66 141-168 141-282 0-17-1-34-4-50 41 48 98 79 162 83a444 444 0 0046-196c0-207-142-382-337-439h-2a33 33 0 01-34-33c0-11 6-21 14-26L596 0C841 51 1024 261 1024 513c0 239-166 441-393 504A35 35 0 01610 1024a33 33 0 01-34-33 33 33 0 0136-33zM512 704a192 192 0 110-384 192 192 0 010 384z M512 64A447 447 0 0064 512c0 248 200 448 448 448s448-200 448-448S760 64 512 64zM218 295h31c54 0 105 19 145 55 13 12 13 31 3 43a35 35 0 01-22 10 36 36 0 01-21-7 155 155 0 00-103-39h-31a32 32 0 01-31-31c0-18 13-31 30-31zm31 433h-31a32 32 0 01-31-31c0-16 13-31 31-31h31A154 154 0 00403 512 217 217 0 01620 295h75l-93-67a33 33 0 01-7-43 33 33 0 0143-7l205 148-205 148a29 29 0 01-18 6 32 32 0 01-31-31c0-10 4-19 13-25l93-67H620a154 154 0 00-154 154c0 122-97 220-217 220zm390 118a29 29 0 01-18 6 32 32 0 01-31-31c0-10 4-19 13-25l93-67h-75c-52 0-103-19-143-54-12-12-13-31-1-43a30 30 0 0142-3 151 151 0 00102 39h75L602 599a33 33 0 01-7-43 33 33 0 0143-7l205 148-203 151z + M922 39H102A65 65 0 0039 106v609a65 65 0 0063 68h94v168a34 34 0 0019 31 30 30 0 0012 3 30 30 0 0022-10l182-192H922a65 65 0 0063-68V106A65 65 0 00922 39zM288 378h479a34 34 0 010 68H288a34 34 0 010-68zm0-135h479a34 34 0 010 68H288a34 34 0 010-68zm0 270h310a34 34 0 010 68H288a34 34 0 010-68z M640 96c-158 0-288 130-288 288 0 17 3 31 5 46L105 681 96 691V928h224v-96h96v-96h96v-95c38 18 82 31 128 31 158 0 288-130 288-288s-130-288-288-288zm0 64c123 0 224 101 224 224s-101 224-224 224a235 235 0 01-109-28l-8-4H448v96h-96v96H256v96H160v-146l253-254 12-11-3-17C419 417 416 400 416 384c0-123 101-224 224-224zm64 96a64 64 0 100 128 64 64 0 100-128z M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zM139 832V192c0-6 4-11 11-11h331v661H149c-6 0-11-4-11-11zm747 0c0 6-4 11-11 11H544v-661H875c6 0 11 4 11 11v640z M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zm-725 64h725c6 0 11 4 11 11v288h-747V192c0-6 4-11 11-11zm725 661H149c-6 0-11-4-11-11V544h747V832c0 6-4 11-11 11z @@ -67,6 +68,8 @@ M0 4 0 20 16 20 0 4M4 0 20 0 20 16 4 0z M192 192m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM192 512m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM192 832m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM864 160H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 480H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 800H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32z M824 645V307c0-56-46-102-102-102h-102V102l-154 154 154 154V307h102v338c-46 20-82 67-82 123 0 72 61 133 133 133 72 0 133-61 133-133 0-56-36-102-82-123zm-51 195c-41 0-72-31-72-72s31-72 72-72c41 0 72 31 72 72s-31 72-72 72zM384 256c0-72-61-133-133-133-72 0-133 61-133 133 0 56 36 102 82 123v266C154 666 118 712 118 768c0 72 61 133 133 133 72 0 133-61 133-133 0-56-36-102-82-123V379C348 358 384 312 384 256zM323 768c0 41-31 72-72 72-41 0-72-31-72-72s31-72 72-72c41 0 72 31 72 72zM251 328c-41 0-72-31-72-72s31-72 72-72c41 0 72 31 72 72s-31 72-72 72z + M896 64H128C96 64 64 96 64 128v768c0 32 32 64 64 64h768c32 0 64-32 64-64V128c0-32-32-64-64-64z m-64 736c0 16-17 32-32 32H224c-18 0-32-12-32-32V224c0-16 16-32 32-32h576c15 0 32 16 32 32v576zM512 384c-71 0-128 57-128 128s57 128 128 128 128-57 128-128-57-128-128-128z + M299 811 299 725 384 725 384 811 299 811M469 811 469 725 555 725 555 811 469 811M640 811 640 725 725 725 725 811 640 811M299 640 299 555 384 555 384 640 299 640M469 640 469 555 555 555 555 640 469 640M640 640 640 555 725 555 725 640 640 640M299 469 299 384 384 384 384 469 299 469M469 469 469 384 555 384 555 469 469 469M640 469 640 384 725 384 725 469 640 469M299 299 299 213 384 213 384 299 299 299M469 299 469 213 555 213 555 299 469 299M640 299 640 213 725 213 725 299 640 299Z M683 409v204L1024 308 683 0v191c-413 0-427 526-427 526c117-229 203-307 427-307zm85 492H102V327h153s38-63 114-122H51c-28 0-51 27-51 61v697c0 34 23 61 51 61h768c28 0 51-27 51-61V614l-102 100v187z M544 85c49 0 90 37 95 85h75a96 96 0 0196 89L811 267a32 32 0 01-28 32L779 299a32 32 0 01-32-28L747 267a32 32 0 00-28-32L715 235h-91a96 96 0 01-80 42H395c-33 0-62-17-80-42L224 235a32 32 0 00-32 28L192 267v576c0 16 12 30 28 32l4 0h128a32 32 0 0132 28l0 4a32 32 0 01-32 32h-128a96 96 0 01-96-89L128 843V267a96 96 0 0189-96L224 171h75a96 96 0 0195-85h150zm256 256a96 96 0 0196 89l0 7v405a96 96 0 01-89 96L800 939h-277a96 96 0 01-96-89L427 843v-405a96 96 0 0189-96L523 341h277zm-256-192H395a32 32 0 000 64h150a32 32 0 100-64z m186 532 287 0 0 287c0 11 9 20 20 20s20-9 20-20l0-287 287 0c11 0 20-9 20-20s-9-20-20-20l-287 0 0-287c0-11-9-20-20-20s-20 9-20 20l0 287-287 0c-11 0-20 9-20 20s9 20 20 20z diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index f8e3f2b3..4a0abd37 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -4,12 +4,12 @@ Info Über SourceGit - • Erstellen mit + • Erstellt mit © 2024 sourcegit-scm - • TextEditor von - • Monospace Schriftarten von - • Quelltext findenst du unter - Opensource & freier Git GUI Client + • Text Editor von + • Monospace-Schriftarten von + • Quelltext findest du unter + Open Source & freier Git GUI Client Worktree hinzufügen Was auschecken: Existierender Branch @@ -19,17 +19,17 @@ Branch Name: Optional. Standard ist der Zielordnername. Branch verfolgen: - Remote Branch verfolgen + Remote-Branch verfolgen Patch Fehler Fehler werfen und anwenden des Patches verweigern Alle Fehler Ähnlich wie 'Fehler', zeigt aber mehr an - Patch Datei: - Wählen Sie anzuwendende .patch Datei + Patch-Datei: + Wähle die anzuwendende .patch-Datei Ignoriere Leerzeichenänderungen Keine Warnungen - Schaltet die Warnung vor überschüssigen Leerzeichen aus + Schaltet die Warnung vor nachgestellte Leerzeichen aus Patch anwenden Warnen Gibt eine Warnung für ein paar solcher Fehler aus, aber wendet es an @@ -38,15 +38,15 @@ Speichere Archiv in: Wähle Archivpfad aus Revision: - Archiv + Archiv erstellen SourceGit Askpass - UNVERÄNDERTE DATEIEN - KEINE UNVERÄNDERTEN DATEIEN GEFUNDEN + ALS UNVERÄNDERT ANGENOMMENE DATEIEN + KEINE UNVERÄNDERT ANGENOMMENEN DATEIEN GEFUNDEN ENTFERNEN BINÄRE DATEI NICHT UNTERSTÜTZT!!! Blame BLAME WIRD BEI DIESER DATEI NICHT UNTERSTÜTZT!!! - Checkout ${0}$... + ${0}$ auschecken... Mit Branch vergleichen Mit HEAD vergleichen Mit Worktree vergleichen @@ -58,12 +58,12 @@ Git Flow - Abschließen ${0}$ Merge ${0}$ in ${1}$ hinein... Pull ${0}$ - Pulle ${0}$ in ${1}$ hinein... + Pull ${0}$ in ${1}$ hinein... Push ${0}$ Rebase ${0}$ auf ${1}$... Benenne ${0}$ um... Setze verfolgten Branch - Upstream Verbindung löschen + Upstream Verfolgung aufheben Branch Vergleich Bytes ABBRECHEN @@ -71,15 +71,15 @@ Zeige als Datei- und Ordnerliste Zeige als Pfadliste Zeige als Dateisystembaum - Checkout Branch - Checkout Commit - Warnung: Beim auschecken eines Commits wird dein HEAD detached sein + Branch auschecken + Commit auschecken + Warnung: Beim Auschecken eines Commits wird dein HEAD losgelöst (detached) sein! Commit: Branch: Lokale Änderungen: Verwerfen Nichts tun - Stash & wieder anwenden + Stashen & wieder anwenden Diesen Commit cherry-picken Commit: Alle Änderungen committen @@ -96,18 +96,18 @@ SCHLIESSEN Editor Diesen Commit cherry-picken - Checkout Commit + Commit auschecken Mit HEAD vergleichen Mit Worktree vergleichen Info kopieren SHA kopieren - Interactives Rebase ${0}$ bis hier - Rebase ${0}$ bis hier - Reset ${0}$ bis hier + Interactives Rebase von ${0}$ auf diesen Commit + Rebase von ${0}$ auf diesen Commit + Reset ${0}$ auf diesen Commit Commit rückgängig machen Umformulieren Als Patch speichern... - Squash in den Parent + Squash in den Vorgänger ÄNDERUNGEN Änderungen durchsuchen... DATEIEN @@ -118,30 +118,39 @@ GEÄNDERT COMMITTER Zeigt nur die ersten 100 Änderungen. Alle Änderungen im ÄNDERUNGEN Tab. - NACHRICHT - PARENTS + COMMIT-NACHRICHT + VORGÄNGER REFS SHA - Commit Nachricht - Beschreibung - Repository konfigurieren + Commit-Nachricht + Details + Repository Einstellungen Email Adresse Email Adresse + GIT + TICKETSYSTEM + Beispiel für Github-Regel hinzufügen + Beispiel für Jira-Regel hinzufügen + Neue Regel + Ticketnummer Regex-Ausdruck: + Name: + Ergebnis-URL: + Verwende bitte $1, $2 um auf Regex-Gruppenwerte zuzugreifen. HTTP Proxy - HTTP proxy für dieses Repository + HTTP Proxy für dieses Repository Benutzername Benutzername für dieses Repository Kopieren - NACHRICHT KOPIEREN + COMMIT-NACHRICHT KOPIEREN Pfad kopieren - Dateiename kopieren + Dateinamen kopieren Branch erstellen... - Basiert auf: + Basierend auf: Erstellten Branch auschecken Lokale Änderungen: Verwerfen Nichts tun - Stash & wieder anwenden + Stashen & wieder anwenden Neuer Branch-Name: Branch-Namen eingeben. Lokalen Branch erstellen @@ -181,7 +190,7 @@ Dateimodus geändert LFS OBJEKT ÄNDERUNG Nächste Änderung - KEINE ÄNDERUNG ODER NUR DATEI-ENDE ÄNDERUNGEN + KEINE ÄNDERUNG ODER NUR ZEILEN-ENDE ÄNDERUNGEN Vorherige Änderung Nebeneinander SUBMODUL @@ -196,7 +205,7 @@ Seiten wechseln Öffne in Merge Tool Änderungen verwerfen - Alle Änderungen in der Working Copy. + Alle Änderungen in der Arbeitskopie. Änderungen: Insgesamt {0} Änderungen werden verworfen Du kannst das nicht rückgängig machen!!! @@ -205,13 +214,13 @@ Ziel: Ausgewählte Gruppe bearbeiten Ausgewähltes Repository bearbeiten - Fast-Forward (ohne Checkout) + Fast-Forward (ohne Auschecken) Fetch Alle Remotes fetchen Ohne Tags fetchen - Alle toten Branches entfernen + Alle toten remote Branches entfernen Remote: - Remote Änderungen fetchen + Remote-Änderungen fetchen Als unverändert annehmen Verwerfen... Verwerfe {0} Dateien... @@ -224,27 +233,27 @@ Stash... {0} Dateien stashen... Unstage - {0} Dateien nicht mehr stashen - Änderungen in ausgewählten Zeilen stashen + {0} Dateien unstagen + Änderungen in ausgewählten Zeilen unstagen "Ihre" verwenden (checkout --theirs) "Meine" verwenden (checkout --ours) Datei Historie FILTER Git-Flow - Development Branch: + Entwicklungs-Branch: Feature: - Feature Prefix: + Feature-Prefix: FLOW - Finish Feature FLOW - Finish Hotfix FLOW - Finish Release Ziel: Hotfix: - Hotfix Prefix: + Hotfix-Prefix: Git-Flow initialisieren Branch behalten - Production Branch: + Produktions-Branch: Release: - Release Prefix: + Release-Prefix: Feature starten... FLOW - Feature starten Hotfix starten... @@ -252,17 +261,17 @@ Name eingeben Release starten... FLOW - Release starten - Versions-Tag Prefix: + Versions-Tag-Prefix: Git LFS Verfolgungsmuster hinzufügen... - Muster ist Dateiname + Muster ist ein Dateiname Eigenes Muster: Verfolgungsmuster zu Git LFS hinzufügen Fetch - LFS Objecte fetchen - Führt `git lfs fetch` aus um Git LFS Objekte herunterzuladen. Das aktualisiert nicht die Working Copy. + LFS Objekte fetchen + Führt `git lfs fetch` aus um Git LFS Objekte herunterzuladen. Das aktualisiert nicht die Arbeitskopie. Installiere Git LFS Hooks - Sperren zeigen + Sperren anzeigen Keine gesperrten Dateien Sperre LFS Sperren @@ -277,35 +286,35 @@ LFS Objekte pushen Pushe große Dateien in der Warteschlange zum Git LFS Endpunkt Remote: - Verfolge Dateien names '{0}' + Verfolge '{0}' benannte Dateien Verfolge alle *{0} Dateien - Historie + Historien Wechsle zwischen horizontalem und vertikalem Layout Wechsle zwischen Kurven- und Konturgraphenmodus AUTOR - GRAPH & SUBJEKT + GRAPH & COMMIT-NACHRICHT SHA - COMMIT ZEIT + COMMIT ZEITPUNKT DURCHSUCHE SHA/SUBJEKT/AUTOR. DRÜCKE ZUM SUCHEN ENTER, ESC UM ABZUBRECHEN LÖSCHEN {0} COMMITS AUSGEWÄHLT Tastaturkürzel Referenz GLOBAL - Aktuelles Popup abbrechen - Aktuelle Seite schließen - Zu vorheriger Seite gehen - Zu nächster Seite gehen - Neue Seite erstellen + Aktuelles Popup schließen + Aktuellen Tab schließen + Zum vorherigen Tab wechseln + Zum nächsten Tab wechseln + Neuen Tab erstellen Einstellungen öffnen REPOSITORY Gestagte Änderungen committen - Gestagte Änderungen Committen und pushen + Gestagte Änderungen committen und pushen Dashboard Modus (Standard) - Erzwinge Neuladen dieses Repositorys + Erzwinge Neuladen des Repositorys Ausgewählte Änderungen stagen/unstagen - Commit Suchmodus + Commit-Suchmodus Wechsle zu 'Änderungen' - Wechsle zu 'Historie' + Wechsle zu 'Historien' Wechsle zu 'Stashes' TEXT EDITOR Suchpanel schließen @@ -322,11 +331,11 @@ Merge request wird durchgeführt. Drücke 'Abbrechen' um den originalen HEAD wiederherzustellen. Rebase wird durchgeführt. Drücke 'Abbrechen' um den originalen HEAD wiederherzustellen. Revert wird durchgeführt. Drücke 'Abbrechen' um den originalen HEAD wiederherzustellen. - Interactiver Rebase + Interaktiver Rebase Ziel Branch: Auf: - Hinaufschieben - Hinunterschieben + Hoch schieben + Runter schieben Source Git FEHLER INFO @@ -345,7 +354,7 @@ Lesezeichen Tab schließen Schließe andere Tabs - Schließe Tabs rechts davon + Schließe Rechte Tabs Kopiere Repository-Pfad Repositories Einfügen @@ -359,27 +368,28 @@ Leztes Jahr Vor {0} Jahren Einstellungen - OBERFLÄCHE + ERSCHEINUNGSBILD Standardschriftart - Standard Schriftgröße - Monospace Schriftart + Standardschriftgröße + Monospace-Schriftart + Verwende nur die Monospace-Schriftart im Text Editor Design - Design Überbrückung + Design-Anpassungen ALLGEMEIN Avatar Server Beim Starten nach Updates suchen Sprache - Commits Historie + Commit-Historie Zuletzt geöffnete Tabs beim Starten wiederherstellen - Betreff Hilfslinienlänge + Commit-Nachricht Hinweislänge Fixe Tab-Breite in Titelleiste Sichtbare Vergleichskontextzeilen GIT Remotes automatisch fetchen - Auto-Fetch Interval + Auto-Fetch Intervall Minute(n) Aktiviere Auto-CRLF - Standard Klon-Ordner + Clone Standardordner Benutzer Email Globale Git Benutzer Email Installationspfad @@ -387,27 +397,27 @@ Benutzername Globaler Git Benutzername Git Version - Git (>= 2.23.0) wird von dieser App benötigt + Diese App setzt Git (>= 2.23.0) voraus GPG SIGNIERUNG - Commit GPG Signierung - Tag GPG Signierung + Commit-Signierung + Tag-Signierung GPG Format - Program Installspfad + GPG Installationspfad Gebe Installationspfad zu installiertem GPG Programm an Benutzer Signierungsschlüssel - Benutzer GPG Signierungsschlüssel + GPG Benutzer Signierungsschlüssel DIFF/MERGE TOOL - Installspfad - Gebe Installationspfad von Diff/Merge Tool an + Installationspfad + Gebe Installationspfad zum Diff/Merge Tool an Tool Remote löschen Ziel: Worktrees löschen Worktree Informationen in `$GIT_DIR/worktrees` löschen Pull - Branch: + Remote-Branch: Alle Branches fetchen - Ziel-Branch: + Lokaler Branch: Lokale Änderungen: Verwerfen Nichts tun @@ -415,14 +425,14 @@ Ohne Tags fetchen Remote: Pull (Fetch & Merge) - Rebase anstatt Merge + Rebase anstatt Merge verwenden Push Erzwinge Push Lokaler Branch: Remote: - Änderungen zum Remote pushen - Remote Branch: - Als verfogender Branch konfigurieren + Push + Remote-Branch: + Remote-Branch verfolgen Alle Tags pushen Tag zum Remote pushen Zu allen Remotes pushen @@ -445,7 +455,7 @@ Bearbeiten... Fetch Im Browser öffnen - Aufräumen (Prune) + Prune Ziel: Bestätige das entfernen des Worktrees Aktiviere `--force` Option @@ -458,9 +468,9 @@ Aufräumen (GC & Prune) Führt `git gc` auf diesem Repository aus. Alles löschen - Dieses Repository konfigureren + Repository Einstellungen WEITER - Repository im Datei-Browser öffnen + Öffne im Datei-Browser GEFILTERT: LOKALE BRANCHES Zum HEAD wechseln @@ -470,14 +480,14 @@ Aktualisiern REMOTES REMOTE HINZUFÜGEN - AUFLÖSEN + LÖSEN Commit suchen Suche über - Datei - Nachricht + Dateiname + Commit-Nachricht SHA Autor & Committer - Duche Branches & Tags + Suche Branches & Tags Statistiken SUBMODULE SUBMODUL HINZUFÜGEN @@ -487,19 +497,19 @@ Öffne im Terminal WORKTREES WORKTREE HINZUFÜGEN - AUFRÄUMEN (PRUNE) + PRUNE Git Repository URL Aktuellen Branch auf Revision zurücksetzen Rücksetzmodus: Verschiebe zu: Aktueller Branch: - Zeige im Datei Explorer - Commit umkehren + Zeige im Datei-Explorer + Commit rückgängig machen Commit: - Commit Änderungen umkehren + Commit Änderungen rückgängig machen Commit Nachricht umformulieren - Verwende 'Shift+Enter' um eine neue Zeile einzufügen. 'Enter' ist das Kürzl für den OK Button - Läuft. Bitte warten... + Verwende 'Shift+Enter' um eine neue Zeile einzufügen. 'Enter' ist das Kürzel für den OK Button + Bitte warten... SPEICHERN Speichern als... Patch wurde erfolgreich gespeichert! @@ -510,15 +520,15 @@ Diese Version überspringen Software Update Es sind momentan kein Updates verfügbar. - Squash HEAD In Parent + Squash HEAD in Vorgänger SSH privater Schlüssel: Pfad zum privaten SSH Schlüssel START Stash Inklusive nicht-verfolgter Dateien - Nachricht: + Name: Optional. Name dieses Stashes - Lokale Änderugen stashen + Lokale Änderungen stashen Anwenden Entfernen Anwenden und entfernen @@ -535,7 +545,7 @@ JAHR COMMITS: COMMITTERS: - SUBMODUL + SUBMODULE Submodul hinzufügen Relativen Pfad kopieren Untergeordnete Submodule fetchen @@ -546,7 +556,7 @@ OK Tag-Namen kopieren Lösche ${0}$... - ${0}$ pushen... + Pushe ${0}$... URL: Submodule aktualisieren Alle Submodule @@ -555,11 +565,12 @@ Submodul: Verwende `--remote` Option Warnung + Willkommensseite Erstelle Gruppe Erstelle Untergruppe Klone Repository Lösche - DRAG & DROP VON ORDNER UNTERSTÜTZT. ANGEPASSTE GRUPPIERUNG UNTERSTÜTZT. + DRAG & DROP VON ORDNER UNTERSTÜTZT. BENUTZERDEFINIERTE GRUPPIERUNG UNTERSTÜTZT. Bearbeiten Öffne alle Repositories Öffne Repository @@ -581,20 +592,20 @@ STRG + Enter KONFLIKTE ERKANNT DATEI KONFLIKTE GELÖST - LETZTE COMMIT NACHRICHTEN + LETZTE COMMIT-NACHRICHTEN NICHT-VERFOLGTE DATEIEN INKLUDIEREN NACHRICHTEN HISTORIE - KEINE BISHERIGEN COMMIT NACHRICHTEN + KEINE BISHERIGEN COMMIT-NACHRICHTEN GESTAGED UNSTAGEN ALLES UNSTAGEN UNSTAGED STAGEN ALLES STAGEN - UNVERÄNDERTE ANZEIGEN + ALS UNVERÄNDERT ANGENOMMENE ANZEIGEN Rechtsklick auf selektierte Dateien und wähle die Konfliktlösungen aus. WORKTREE - Path kopieren + Pfad kopieren Sperren Entfernen Entsperren diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index d95d4c74..364494f0 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -124,6 +124,15 @@ Repository Configure Email Address Email address + GIT + ISSUE TRACKER + Add Sample Github Rule + Add Sample Jira Rule + New Rule + Issue Regex Expression: + Rule Name: + Result URL: + Please use $1, $2 to access regex groups values. HTTP Proxy HTTP proxy used by this repository User Name @@ -476,6 +485,7 @@ SHA Author & Committer Search Branches & Tags + Show Tags as Tree Statistics SUBMODULES ADD SUBMODULE diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 537c0d6c..58e915db 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -127,6 +127,15 @@ 仓库配置 电子邮箱 邮箱地址 + GIT配置 + ISSUE追踪 + 新增匹配Github Issue规则 + 新增匹配Jira规则 + 新增自定义规则 + 匹配ISSUE的正则表达式 : + 规则名 : + 为ISSUE生成的URL链接 : + 可在URL中使用$1,$2等变量填入正则表达式匹配的内容 HTTP代理 HTTP网络代理 用户名 @@ -478,6 +487,7 @@ 提交指纹 作者及提交者 快速查找分支、标签 + 以树型结构展示 提交统计 子模块列表 添加子模块 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 25df2430..167933da 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -127,6 +127,15 @@ 倉庫配置 電子郵箱 郵箱地址 + GIT配置 + ISSUE追蹤 + 新增匹配Github Issue規則 + 新增匹配Jira規則 + 新增自定義規則 + 匹配ISSUE的正則表達式 : + 規則名 : + 為ISSUE生成的URL連結 : + 可在URL中使用$1,$2等變數填入正則表示式匹配的內容 HTTP代理 HTTP網路代理 使用者名稱 @@ -478,6 +487,7 @@ 提交指紋 作者及提交者 快速查找分支、標籤 + 以樹型結構展示 提交統計 子模組列表 新增子模組 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index ad443a2c..f40e88f6 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -164,6 +164,7 @@ + @@ -276,6 +277,13 @@ + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -312,7 +255,7 @@ IsReadOnly="True" HeadersVisibility="None" Focusable="False" - RowHeight="26" + RowHeight="24" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" ContextRequested="OnSubmoduleContextRequested" @@ -322,6 +265,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/RepositoryConfigure.axaml.cs b/src/Views/RepositoryConfigure.axaml.cs index 0faa943b..b309453d 100644 --- a/src/Views/RepositoryConfigure.axaml.cs +++ b/src/Views/RepositoryConfigure.axaml.cs @@ -16,11 +16,6 @@ namespace SourceGit.Views } private void CloseWindow(object _1, RoutedEventArgs _2) - { - Close(); - } - - private void SaveAndClose(object _1, RoutedEventArgs _2) { (DataContext as ViewModels.RepositoryConfigure)?.Save(); Close(); diff --git a/src/Views/RevisionFiles.axaml.cs b/src/Views/RevisionFiles.axaml.cs index 227c11a1..b76e1360 100644 --- a/src/Views/RevisionFiles.axaml.cs +++ b/src/Views/RevisionFiles.axaml.cs @@ -9,6 +9,7 @@ using Avalonia.Media; using AvaloniaEdit; using AvaloniaEdit.Document; using AvaloniaEdit.Editing; +using AvaloniaEdit.TextMate; namespace SourceGit.Views { @@ -35,6 +36,7 @@ namespace SourceGit.Views base.OnLoaded(e); TextArea.TextView.ContextRequested += OnTextViewContextRequested; + UpdateTextMate(); } protected override void OnUnloaded(RoutedEventArgs e) @@ -42,6 +44,13 @@ namespace SourceGit.Views base.OnUnloaded(e); TextArea.TextView.ContextRequested -= OnTextViewContextRequested; + + if (_textMate != null) + { + _textMate.Dispose(); + _textMate = null; + } + GC.Collect(); } @@ -50,9 +59,14 @@ namespace SourceGit.Views base.OnDataContextChanged(e); if (DataContext is Models.RevisionTextFile source) + { + UpdateTextMate(); Text = source.Content; + } else + { Text = string.Empty; + } } private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) @@ -85,6 +99,17 @@ namespace SourceGit.Views TextArea.TextView.OpenContextMenu(menu); e.Handled = true; } + + private void UpdateTextMate() + { + if (_textMate == null) + _textMate = Models.TextMateHelper.CreateForEditor(this); + + if (DataContext is Models.RevisionTextFile file) + Models.TextMateHelper.SetGrammarByFileName(_textMate, file.FileName); + } + + private TextMate.Installation _textMate = null; } public partial class RevisionFiles : UserControl diff --git a/src/Views/Statistics.axaml b/src/Views/Statistics.axaml index bb1dbf26..ceb2de31 100644 --- a/src/Views/Statistics.axaml +++ b/src/Views/Statistics.axaml @@ -55,7 +55,11 @@ SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" HorizontalAlignment="Center" VerticalAlignment="Center" - Background="Transparent"> + Background="Transparent" + BorderThickness="1" + BorderBrush="{DynamicResource Brush.Border2}" + CornerRadius="14" + Padding="3,0"> @@ -64,6 +68,7 @@ @@ -77,12 +82,15 @@ + + - + - + - + diff --git a/src/Views/TagsView.axaml b/src/Views/TagsView.axaml new file mode 100644 index 00000000..bcbbe358 --- /dev/null +++ b/src/Views/TagsView.axaml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/TagsView.axaml.cs b/src/Views/TagsView.axaml.cs new file mode 100644 index 00000000..23d31ab4 --- /dev/null +++ b/src/Views/TagsView.axaml.cs @@ -0,0 +1,324 @@ +using System; +using System.Collections.Generic; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public class TagTreeNodeToggleButton : ToggleButton + { + protected override Type StyleKeyOverride => typeof(ToggleButton); + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && + DataContext is ViewModels.TagTreeNode { IsFolder: true } node) + { + var view = this.FindAncestorOfType(); + view?.ToggleNodeIsExpanded(node); + } + + e.Handled = true; + } + } + + public class TagTreeNodeIcon : UserControl + { + public static readonly StyledProperty NodeProperty = + AvaloniaProperty.Register(nameof(Node)); + + public ViewModels.TagTreeNode Node + { + get => GetValue(NodeProperty); + set => SetValue(NodeProperty, value); + } + + public static readonly StyledProperty IsExpandedProperty = + AvaloniaProperty.Register(nameof(IsExpanded)); + + public bool IsExpanded + { + get => GetValue(IsExpandedProperty); + set => SetValue(IsExpandedProperty, value); + } + + static TagTreeNodeIcon() + { + NodeProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent()); + IsExpandedProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent()); + } + + private void UpdateContent() + { + var node = Node; + if (node == null) + { + Content = null; + return; + } + + if (node.Tag != null) + CreateContent(new Thickness(0, 2, 0, 0), "Icons.Tag"); + else if (node.IsExpanded) + CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder.Open"); + else + CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder"); + } + + private void CreateContent(Thickness margin, string iconKey) + { + var geo = this.FindResource(iconKey) as StreamGeometry; + if (geo == null) + return; + + Content = new Avalonia.Controls.Shapes.Path() + { + Width = 12, + Height = 12, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = margin, + Data = geo, + }; + } + } + + public partial class TagsView : UserControl + { + public static readonly StyledProperty ShowTagsAsTreeProperty = + AvaloniaProperty.Register(nameof(ShowTagsAsTree)); + + public bool ShowTagsAsTree + { + get => GetValue(ShowTagsAsTreeProperty); + set => SetValue(ShowTagsAsTreeProperty, value); + } + + public static readonly StyledProperty> TagsProperty = + AvaloniaProperty.Register>(nameof(Tags)); + + public List Tags + { + get => GetValue(TagsProperty); + set => SetValue(TagsProperty, value); + } + + public static readonly RoutedEvent SelectionChangedEvent = + RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + public event EventHandler SelectionChanged + { + add { AddHandler(SelectionChangedEvent, value); } + remove { RemoveHandler(SelectionChangedEvent, value); } + } + + public static readonly RoutedEvent RowsChangedEvent = + RoutedEvent.Register(nameof(RowsChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + public event EventHandler RowsChanged + { + add { AddHandler(RowsChangedEvent, value); } + remove { RemoveHandler(RowsChangedEvent, value); } + } + + public int Rows + { + get; + private set; + } + + public TagsView() + { + InitializeComponent(); + } + + public void UnselectAll() + { + var list = this.FindDescendantOfType(); + if (list != null) + list.SelectedItem = null; + } + + public void ToggleNodeIsExpanded(ViewModels.TagTreeNode node) + { + if (Content is ViewModels.TagCollectionAsTree tree) + { + node.IsExpanded = !node.IsExpanded; + + var depth = node.Depth; + var idx = tree.Rows.IndexOf(node); + if (idx == -1) + return; + + if (node.IsExpanded) + { + var subrows = new List(); + MakeTreeRows(subrows, node.Children); + tree.Rows.InsertRange(idx + 1, subrows); + } + else + { + var removeCount = 0; + for (int i = idx + 1; i < tree.Rows.Count; i++) + { + var row = tree.Rows[i]; + if (row.Depth <= depth) + break; + + removeCount++; + } + tree.Rows.RemoveRange(idx + 1, removeCount); + } + + Rows = tree.Rows.Count; + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ShowTagsAsTreeProperty || change.Property == TagsProperty) + { + UpdateDataSource(); + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); + } + else if (change.Property == IsVisibleProperty) + { + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); + } + } + + private void OnDoubleTappedNode(object sender, TappedEventArgs e) + { + if (sender is Grid { DataContext: ViewModels.TagTreeNode node }) + { + if (node.IsFolder) + ToggleNodeIsExpanded(node); + } + + e.Handled = true; + } + + private void OnRowContextRequested(object sender, ContextRequestedEventArgs e) + { + var control = sender as Control; + if (control == null) + return; + + Models.Tag selected; + if (control.DataContext is ViewModels.TagTreeNode node) + selected = node.Tag; + else if (control.DataContext is Models.Tag tag) + selected = tag; + else + selected = null; + + if (selected != null && DataContext is ViewModels.Repository repo) + { + var menu = repo.CreateContextMenuForTag(selected); + control.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private void OnRowSelectionChanged(object sender, SelectionChangedEventArgs _) + { + var selected = (sender as ListBox)?.SelectedItem; + var selectedTag = null as Models.Tag; + if (selected is ViewModels.TagTreeNode node) + selectedTag = node.Tag; + else if (selected is Models.Tag tag) + selectedTag = tag; + + if (selectedTag != null && DataContext is ViewModels.Repository repo) + { + RaiseEvent(new RoutedEventArgs(SelectionChangedEvent)); + repo.NavigateToCommit(selectedTag.SHA); + } + } + + private void OnToggleFilter(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton toggle && DataContext is ViewModels.Repository repo) + { + var target = null as Models.Tag; + if (toggle.DataContext is ViewModels.TagTreeNode node) + target = node.Tag; + else if (toggle.DataContext is Models.Tag tag) + target = tag; + + if (target != null) + repo.UpdateFilter(target.Name, toggle.IsChecked == true); + } + + e.Handled = true; + } + + private void MakeTreeRows(List rows, List nodes) + { + foreach (var node in nodes) + { + rows.Add(node); + + if (!node.IsExpanded || !node.IsFolder) + continue; + + MakeTreeRows(rows, node.Children); + } + } + + private void UpdateDataSource() + { + var tags = Tags; + if (tags == null || tags.Count == 0) + { + Content = null; + return; + } + + if (ShowTagsAsTree) + { + var oldExpanded = new HashSet(); + if (Content is ViewModels.TagCollectionAsTree oldTree) + { + foreach (var row in oldTree.Rows) + { + if (row.IsFolder && row.IsExpanded) + oldExpanded.Add(row.FullPath); + } + } + + var tree = new ViewModels.TagCollectionAsTree(); + tree.Tree = ViewModels.TagTreeNode.Build(tags, oldExpanded); + + var rows = new List(); + MakeTreeRows(rows, tree.Tree); + tree.Rows.AddRange(rows); + + Content = tree; + Rows = rows.Count; + } + else + { + var list = new ViewModels.TagCollectionAsList(); + list.Tags.AddRange(tags); + + Content = list; + Rows = tags.Count; + } + + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); + } + } +} + diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index 0a97e051..d5f6194a 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -1085,7 +1085,7 @@ namespace SourceGit.Views return; } - var top = chunk.Y + 16; + var top = chunk.Y + (chunk.Height >= 36 ? 16 : 4); var right = (chunk.Combined || !chunk.IsOldSide) ? 16 : v.Bounds.Width * 0.5f + 16; v.Popup.Margin = new Thickness(0, top, right, 0); v.Popup.IsVisible = true; @@ -1147,27 +1147,22 @@ namespace SourceGit.Views if (!selection.HasChanges) return; + var repoView = this.FindAncestorOfType(); + if (repoView == null) + return; + + var repo = repoView.DataContext as ViewModels.Repository; + if (repo == null) + return; + + repo.SetWatcherEnabled(false); + if (!selection.HasLeftChanges) { - var workcopyView = this.FindAncestorOfType(); - if (workcopyView == null) - return; - - var workcopy = workcopyView.DataContext as ViewModels.WorkingCopy; - workcopy?.StageChanges(new List { change }); + new Commands.Add(repo.FullPath, [change]).Exec(); } else { - var repoView = this.FindAncestorOfType(); - if (repoView == null) - return; - - var repo = repoView.DataContext as ViewModels.Repository; - if (repo == null) - return; - - repo.SetWatcherEnabled(false); - var tmpFile = Path.GetTempFileName(); if (change.WorkTree == Models.ChangeState.Untracked) { @@ -1186,10 +1181,10 @@ namespace SourceGit.Views new Commands.Apply(diff.Repo, tmpFile, true, "nowarn", "--cache --index").Exec(); File.Delete(tmpFile); - - repo.MarkWorkingCopyDirtyManually(); - repo.SetWatcherEnabled(true); } + + repo.MarkWorkingCopyDirtyManually(); + repo.SetWatcherEnabled(true); } private void OnUnstageChunk(object sender, RoutedEventArgs e) @@ -1210,27 +1205,25 @@ namespace SourceGit.Views if (!selection.HasChanges) return; + var repoView = this.FindAncestorOfType(); + if (repoView == null) + return; + + var repo = repoView.DataContext as ViewModels.Repository; + if (repo == null) + return; + + repo.SetWatcherEnabled(false); + if (!selection.HasLeftChanges) { - var workcopyView = this.FindAncestorOfType(); - if (workcopyView == null) - return; - - var workcopy = workcopyView.DataContext as ViewModels.WorkingCopy; - workcopy?.UnstageChanges(new List { change }); + if (change.DataForAmend != null) + new Commands.UnstageChangesForAmend(repo.FullPath, [change]).Exec(); + else + new Commands.Reset(repo.FullPath, [change]).Exec(); } else { - var repoView = this.FindAncestorOfType(); - if (repoView == null) - return; - - var repo = repoView.DataContext as ViewModels.Repository; - if (repo == null) - return; - - repo.SetWatcherEnabled(false); - var treeGuid = new Commands.QueryStagedFileBlobGuid(diff.Repo, change.Path).Result(); var tmpFile = Path.GetTempFileName(); if (change.Index == Models.ChangeState.Added) @@ -1242,10 +1235,10 @@ namespace SourceGit.Views new Commands.Apply(diff.Repo, tmpFile, true, "nowarn", "--cache --index --reverse").Exec(); File.Delete(tmpFile); - - repo.MarkWorkingCopyDirtyManually(); - repo.SetWatcherEnabled(true); } + + repo.MarkWorkingCopyDirtyManually(); + repo.SetWatcherEnabled(true); } private void OnDiscardChunk(object sender, RoutedEventArgs e) @@ -1266,27 +1259,22 @@ namespace SourceGit.Views if (!selection.HasChanges) return; + var repoView = this.FindAncestorOfType(); + if (repoView == null) + return; + + var repo = repoView.DataContext as ViewModels.Repository; + if (repo == null) + return; + + repo.SetWatcherEnabled(false); + if (!selection.HasLeftChanges) { - var workcopyView = this.FindAncestorOfType(); - if (workcopyView == null) - return; - - var workcopy = workcopyView.DataContext as ViewModels.WorkingCopy; - workcopy?.Discard(new List { change }); + Commands.Discard.Changes(repo.FullPath, [change]); } else { - var repoView = this.FindAncestorOfType(); - if (repoView == null) - return; - - var repo = repoView.DataContext as ViewModels.Repository; - if (repo == null) - return; - - repo.SetWatcherEnabled(false); - var tmpFile = Path.GetTempFileName(); if (change.Index == Models.ChangeState.Added) { @@ -1305,10 +1293,10 @@ namespace SourceGit.Views new Commands.Apply(diff.Repo, tmpFile, true, "nowarn", "--reverse").Exec(); File.Delete(tmpFile); - - repo.MarkWorkingCopyDirtyManually(); - repo.SetWatcherEnabled(true); } + + repo.MarkWorkingCopyDirtyManually(); + repo.SetWatcherEnabled(true); } } } diff --git a/src/Views/WelcomeToolbar.axaml.cs b/src/Views/WelcomeToolbar.axaml.cs index e0a6acfb..2f8de643 100644 --- a/src/Views/WelcomeToolbar.axaml.cs +++ b/src/Views/WelcomeToolbar.axaml.cs @@ -55,7 +55,7 @@ namespace SourceGit.Views } var normalizedPath = root.Replace("\\", "/"); - var node = ViewModels.Preference.Instance.FindOrAddNodeByRepositoryPath(normalizedPath, parent, true); + var node = ViewModels.Preference.Instance.FindOrAddNodeByRepositoryPath(normalizedPath, parent, false); var launcher = this.FindAncestorOfType()?.DataContext as ViewModels.Launcher; launcher?.OpenRepositoryInTab(node, launcher.ActivePage); }