diff --git a/README.md b/README.md
index 50d00f58..2d222996 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@
* Search commits
* GitFlow
* Git LFS
+* Bisect
* Issue Link
* Workspace
* Custom Action
diff --git a/TRANSLATION.md b/TRANSLATION.md
index 866aac65..21706eb2 100644
--- a/TRANSLATION.md
+++ b/TRANSLATION.md
@@ -6,11 +6,18 @@ This document shows the translation status of each locale file in the repository
### 
-### 
+### 
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
@@ -29,6 +36,7 @@ This document shows the translation status of each locale file in the repository
- Text.Preferences.AI.Streaming
- Text.Preferences.Appearance.EditorTabWidth
- Text.Preferences.General.ShowTagsInGraph
+- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Repository.ViewLogs
- Text.StashCM.SaveAsPatch
- Text.ViewLogs
@@ -43,27 +51,20 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
-
-Missing keys in es_ES.axaml
-
-- Text.CommitCM.CopyAuthor
-- Text.CommitCM.CopyCommitter
-- Text.CommitCM.CopySubject
-- Text.Repository.ViewLogs
-- Text.ViewLogs
-- Text.ViewLogs.Clear
-- Text.ViewLogs.CopyLog
-- Text.ViewLogs.Delete
-
-
-
-### 
+### 
Missing keys in fr_FR.axaml
+- Text.Bisect
+- Text.Bisect.Abort
+- Text.Bisect.Bad
+- Text.Bisect.Detecting
+- Text.Bisect.Good
+- Text.Bisect.Skip
+- Text.Bisect.WaitingForRange
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
@@ -73,6 +74,7 @@ This document shows the translation status of each locale file in the repository
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
+- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Repository.ViewLogs
- Text.ViewLogs
- Text.ViewLogs.Clear
@@ -86,11 +88,18 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
Missing keys in it_IT.axaml
+- Text.Bisect
+- Text.Bisect.Abort
+- Text.Bisect.Bad
+- Text.Bisect.Detecting
+- Text.Bisect.Good
+- Text.Bisect.Skip
+- Text.Bisect.WaitingForRange
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
@@ -102,6 +111,7 @@ This document shows the translation status of each locale file in the repository
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CopyFullPath
- Text.Preferences.General.ShowTagsInGraph
+- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Repository.ViewLogs
- Text.ViewLogs
- Text.ViewLogs.Clear
@@ -115,11 +125,18 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
Missing keys in ja_JP.axaml
+- Text.Bisect
+- Text.Bisect.Abort
+- Text.Bisect.Bad
+- Text.Bisect.Detecting
+- Text.Bisect.Good
+- Text.Bisect.Skip
+- Text.Bisect.WaitingForRange
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
@@ -129,6 +146,7 @@ This document shows the translation status of each locale file in the repository
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
+- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Repository.FilterCommits
- Text.Repository.Tags.OrderByNameDes
- Text.Repository.ViewLogs
@@ -144,7 +162,7 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
Missing keys in pt_BR.axaml
@@ -155,6 +173,13 @@ This document shows the translation status of each locale file in the repository
- Text.ApplyStash.DropAfterApply
- Text.ApplyStash.RestoreIndex
- Text.ApplyStash.Stash
+- Text.Bisect
+- Text.Bisect.Abort
+- Text.Bisect.Bad
+- Text.Bisect.Detecting
+- Text.Bisect.Good
+- Text.Bisect.Skip
+- Text.Bisect.WaitingForRange
- Text.BranchCM.CustomAction
- Text.BranchCM.MergeMultiBranches
- Text.BranchUpstreamInvalid
@@ -201,6 +226,7 @@ This document shows the translation status of each locale file in the repository
- Text.Preferences.General.DateFormat
- Text.Preferences.General.ShowChildren
- Text.Preferences.General.ShowTagsInGraph
+- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Preferences.Git.SSLVerify
- Text.Repository.FilterCommits
- Text.Repository.HistoriesLayout
@@ -238,28 +264,20 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
-
-Missing keys in ru_RU.axaml
-
-- Text.CommitCM.CopyAuthor
-- Text.CommitCM.CopyCommitter
-- Text.CommitCM.CopySubject
-- Text.CommitMessageTextBox.SubjectCount
-- Text.Repository.ViewLogs
-- Text.ViewLogs
-- Text.ViewLogs.Clear
-- Text.ViewLogs.CopyLog
-- Text.ViewLogs.Delete
-
-
-
-### 
+### 
Missing keys in ta_IN.axaml
+- Text.Bisect
+- Text.Bisect.Abort
+- Text.Bisect.Bad
+- Text.Bisect.Detecting
+- Text.Bisect.Good
+- Text.Bisect.Skip
+- Text.Bisect.WaitingForRange
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
@@ -269,6 +287,7 @@ This document shows the translation status of each locale file in the repository
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
+- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Repository.ViewLogs
- Text.UpdateSubmodules.Target
- Text.ViewLogs
@@ -282,16 +301,24 @@ This document shows the translation status of each locale file in the repository
-### 
+### 
Missing keys in uk_UA.axaml
+- Text.Bisect
+- Text.Bisect.Abort
+- Text.Bisect.Bad
+- Text.Bisect.Detecting
+- Text.Bisect.Good
+- Text.Bisect.Skip
+- Text.Bisect.WaitingForRange
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitMessageTextBox.SubjectCount
- Text.ConfigureWorkspace.Name
+- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Repository.ViewLogs
- Text.ViewLogs
- Text.ViewLogs.Clear
diff --git a/VERSION b/VERSION
index 89f9e970..70f2a59f 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2025.14
\ No newline at end of file
+2025.15
\ No newline at end of file
diff --git a/src/App.Commands.cs b/src/App.Commands.cs
index 85a75829..22e9fb51 100644
--- a/src/App.Commands.cs
+++ b/src/App.Commands.cs
@@ -37,10 +37,10 @@ namespace SourceGit
}
}
- public static readonly Command OpenPreferencesCommand = new Command(_ => OpenDialog(new Views.Preferences()));
- public static readonly Command OpenHotkeysCommand = new Command(_ => OpenDialog(new Views.Hotkeys()));
+ public static readonly Command OpenPreferencesCommand = new Command(_ => ShowWindow(new Views.Preferences(), false));
+ public static readonly Command OpenHotkeysCommand = new Command(_ => ShowWindow(new Views.Hotkeys(), false));
public static readonly Command OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir));
- public static readonly Command OpenAboutCommand = new Command(_ => OpenDialog(new Views.About()));
+ public static readonly Command OpenAboutCommand = new Command(_ => ShowWindow(new Views.About(), false));
public static readonly Command CheckForUpdateCommand = new Command(_ => (Current as App)?.Check4Update(true));
public static readonly Command QuitCommand = new Command(_ => Quit(0));
public static readonly Command CopyTextBlockCommand = new Command(p =>
diff --git a/src/App.axaml b/src/App.axaml
index 73b97017..186022d5 100644
--- a/src/App.axaml
+++ b/src/App.axaml
@@ -35,7 +35,7 @@
-
+
diff --git a/src/App.axaml.cs b/src/App.axaml.cs
index c659388a..822084dc 100644
--- a/src/App.axaml.cs
+++ b/src/App.axaml.cs
@@ -105,10 +105,36 @@ namespace SourceGit
#endregion
#region Utility Functions
- public static void OpenDialog(Window window)
+ public static void ShowWindow(object data, bool showAsDialog)
{
- if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
- window.ShowDialog(owner);
+ if (data is Views.ChromelessWindow window)
+ {
+ if (showAsDialog && Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
+ window.ShowDialog(owner);
+ else
+ window.Show();
+
+ return;
+ }
+
+ var dataTypeName = data.GetType().FullName;
+ if (string.IsNullOrEmpty(dataTypeName) || !dataTypeName.Contains(".ViewModels.", StringComparison.Ordinal))
+ return;
+
+ var viewTypeName = dataTypeName.Replace(".ViewModels.", ".Views.");
+ var viewType = Type.GetType(viewTypeName);
+ if (viewType == null || !viewType.IsSubclassOf(typeof(Views.ChromelessWindow)))
+ return;
+
+ window = Activator.CreateInstance(viewType) as Views.ChromelessWindow;
+ if (window != null)
+ {
+ window.DataContext = data;
+ if (showAsDialog && Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
+ window.ShowDialog(owner);
+ else
+ window.Show();
+ }
}
public static void RaiseException(string context, string message)
@@ -342,6 +368,14 @@ namespace SourceGit
{
BindingPlugins.DataValidators.RemoveAt(0);
+ // Disable tooltip if window is not active.
+ ToolTip.ToolTipOpeningEvent.AddClassHandler((c, e) =>
+ {
+ var topLevel = TopLevel.GetTopLevel(c);
+ if (topLevel is not Window { IsActive: true })
+ e.Cancel = true;
+ });
+
if (TryLaunchAsCoreEditor(desktop))
return;
@@ -445,7 +479,7 @@ namespace SourceGit
if (!collection.Onto.Equals(onto) || !collection.OrigHead.Equals(origHead))
return true;
- var done = File.ReadAllText(doneFile).Trim().Split([ '\r', '\n' ], StringSplitOptions.RemoveEmptyEntries);
+ var done = File.ReadAllText(doneFile).Trim().Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
if (done.Length == 0)
return true;
@@ -598,11 +632,7 @@ namespace SourceGit
{
Dispatcher.UIThread.Post(() =>
{
- if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
- {
- var dialog = new Views.SelfUpdate() { DataContext = new ViewModels.SelfUpdate() { Data = data } };
- dialog.ShowDialog(owner);
- }
+ ShowWindow(new ViewModels.SelfUpdate() { Data = data }, true);
});
}
diff --git a/src/Commands/Bisect.cs b/src/Commands/Bisect.cs
new file mode 100644
index 00000000..a3bf1a97
--- /dev/null
+++ b/src/Commands/Bisect.cs
@@ -0,0 +1,13 @@
+namespace SourceGit.Commands
+{
+ public class Bisect : Command
+ {
+ public Bisect(string repo, string subcmd)
+ {
+ WorkingDirectory = repo;
+ Context = repo;
+ RaiseError = false;
+ Args = $"bisect {subcmd}";
+ }
+ }
+}
diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs
index 65a2a6f5..a60f4cc5 100644
--- a/src/Commands/Diff.cs
+++ b/src/Commands/Diff.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
@@ -28,7 +28,9 @@ namespace SourceGit.Commands
Context = repo;
if (ignoreWhitespace)
- Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --ignore-cr-at-eol --ignore-all-space --unified={unified} {opt}";
+ Args = $"-c core.autocrlf=false 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}";
else
Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --unified={unified} {opt}";
}
@@ -103,7 +105,7 @@ namespace SourceGit.Commands
}
else if (line.StartsWith("-size ", StringComparison.Ordinal))
{
- _result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
+ _result.LFSDiff.Old.Size = long.Parse(line.AsSpan().Slice(6));
}
}
else if (ch == '+')
@@ -114,12 +116,12 @@ namespace SourceGit.Commands
}
else if (line.StartsWith("+size ", StringComparison.Ordinal))
{
- _result.LFSDiff.New.Size = long.Parse(line.Substring(6));
+ _result.LFSDiff.New.Size = long.Parse(line.AsSpan().Slice(6));
}
}
else if (line.StartsWith(" size ", StringComparison.Ordinal))
{
- _result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
+ _result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.AsSpan().Slice(6));
}
return;
}
diff --git a/src/Commands/QueryStagedChangesWithAmend.cs b/src/Commands/QueryStagedChangesWithAmend.cs
index cfea5e35..8f8456c9 100644
--- a/src/Commands/QueryStagedChangesWithAmend.cs
+++ b/src/Commands/QueryStagedChangesWithAmend.cs
@@ -11,11 +11,12 @@ namespace SourceGit.Commands
[GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} R\d{0,6}\t(.*\t.*)$")]
private static partial Regex REG_FORMAT2();
- public QueryStagedChangesWithAmend(string repo)
+ public QueryStagedChangesWithAmend(string repo, string parent)
{
WorkingDirectory = repo;
Context = repo;
- Args = "diff-index --cached -M HEAD^";
+ Args = $"diff-index --cached -M {parent}";
+ _parent = parent;
}
public List Result()
@@ -37,6 +38,7 @@ namespace SourceGit.Commands
{
FileMode = match.Groups[1].Value,
ObjectHash = match.Groups[2].Value,
+ ParentSHA = _parent,
},
};
change.Set(Models.ChangeState.Renamed);
@@ -54,6 +56,7 @@ namespace SourceGit.Commands
{
FileMode = match.Groups[1].Value,
ObjectHash = match.Groups[2].Value,
+ ParentSHA = _parent,
},
};
@@ -88,5 +91,7 @@ namespace SourceGit.Commands
return [];
}
+
+ private string _parent = string.Empty;
}
}
diff --git a/src/Models/Bisect.cs b/src/Models/Bisect.cs
new file mode 100644
index 00000000..d1021113
--- /dev/null
+++ b/src/Models/Bisect.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+
+namespace SourceGit.Models
+{
+ public enum BisectState
+ {
+ None = 0,
+ WaitingForRange,
+ Detecting,
+ }
+
+ [Flags]
+ public enum BisectCommitFlag
+ {
+ None = 0,
+ Good = 1,
+ Bad = 2,
+ }
+
+ public class Bisect
+ {
+ public HashSet Bads
+ {
+ get;
+ set;
+ } = [];
+
+ public HashSet Goods
+ {
+ get;
+ set;
+ } = [];
+ }
+}
diff --git a/src/Models/Change.cs b/src/Models/Change.cs
index e9d07181..0c96ec95 100644
--- a/src/Models/Change.cs
+++ b/src/Models/Change.cs
@@ -26,6 +26,7 @@ namespace SourceGit.Models
{
public string FileMode { get; set; } = "";
public string ObjectHash { get; set; } = "";
+ public string ParentSHA { get; set; } = "";
}
public class Change
diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs
index 0bad8376..1980e622 100644
--- a/src/Models/Commit.cs
+++ b/src/Models/Commit.cs
@@ -117,6 +117,6 @@ namespace SourceGit.Models
public class CommitFullMessage
{
public string Message { get; set; } = string.Empty;
- public List Links { get; set; } = [];
+ public List Inlines { get; set; } = [];
}
}
diff --git a/src/Models/CommitLink.cs b/src/Models/CommitLink.cs
index 955779a8..544ee3c3 100644
--- a/src/Models/CommitLink.cs
+++ b/src/Models/CommitLink.cs
@@ -1,8 +1,49 @@
-namespace SourceGit.Models
+using System;
+using System.Collections.Generic;
+
+namespace SourceGit.Models
{
public class CommitLink
{
public string Name { get; set; } = null;
public string URLPrefix { get; set; } = null;
+
+ public CommitLink(string name, string prefix)
+ {
+ Name = name;
+ URLPrefix = prefix;
+ }
+
+ public static List Get(List remotes)
+ {
+ var outs = new List();
+
+ foreach (var remote in remotes)
+ {
+ if (remote.TryGetVisitURL(out var url))
+ {
+ var trimmedUrl = url;
+ if (url.EndsWith(".git"))
+ trimmedUrl = url.Substring(0, url.Length - 4);
+
+ if (url.StartsWith("https://github.com/", StringComparison.Ordinal))
+ outs.Add(new($"Github ({trimmedUrl.Substring(19)})", $"{url}/commit/"));
+ else if (url.StartsWith("https://gitlab.", StringComparison.Ordinal))
+ outs.Add(new($"GitLab ({trimmedUrl.Substring(trimmedUrl.Substring(15).IndexOf('/') + 16)})", $"{url}/-/commit/"));
+ else if (url.StartsWith("https://gitee.com/", StringComparison.Ordinal))
+ outs.Add(new($"Gitee ({trimmedUrl.Substring(18)})", $"{url}/commit/"));
+ else if (url.StartsWith("https://bitbucket.org/", StringComparison.Ordinal))
+ outs.Add(new($"BitBucket ({trimmedUrl.Substring(22)})", $"{url}/commits/"));
+ else if (url.StartsWith("https://codeberg.org/", StringComparison.Ordinal))
+ outs.Add(new($"Codeberg ({trimmedUrl.Substring(21)})", $"{url}/commit/"));
+ else if (url.StartsWith("https://gitea.org/", StringComparison.Ordinal))
+ outs.Add(new($"Gitea ({trimmedUrl.Substring(18)})", $"{url}/commit/"));
+ else if (url.StartsWith("https://git.sr.ht/", StringComparison.Ordinal))
+ outs.Add(new($"sourcehut ({trimmedUrl.Substring(18)})", $"{url}/commit/"));
+ }
+ }
+
+ return outs;
+ }
}
}
diff --git a/src/Models/ConventionalCommitType.cs b/src/Models/ConventionalCommitType.cs
index cd09453a..531a16c0 100644
--- a/src/Models/ConventionalCommitType.cs
+++ b/src/Models/ConventionalCommitType.cs
@@ -4,25 +4,24 @@ namespace SourceGit.Models
{
public class ConventionalCommitType
{
- public string Name { get; set; } = string.Empty;
- public string Type { get; set; } = string.Empty;
- public string Description { get; set; } = string.Empty;
+ public string Name { get; set; }
+ public string Type { get; set; }
+ public string Description { get; set; }
- public static readonly List Supported = new List()
- {
- new ConventionalCommitType("Features", "feat", "Adding a new feature"),
- new ConventionalCommitType("Bug Fixes", "fix", "Fixing a bug"),
- new ConventionalCommitType("Work In Progress", "wip", "Still being developed and not yet complete"),
- new ConventionalCommitType("Reverts", "revert", "Undoing a previous commit"),
- new ConventionalCommitType("Code Refactoring", "refactor", "Restructuring code without changing its external behavior"),
- new ConventionalCommitType("Performance Improvements", "pref", "Improves performance"),
- new ConventionalCommitType("Builds", "build", "Changes that affect the build system or external dependencies"),
- new ConventionalCommitType("Continuous Integrations", "ci", "Changes to CI configuration files and scripts"),
- new ConventionalCommitType("Documentations", "docs", "Updating documentation"),
- new ConventionalCommitType("Styles", "style", "Elements or code styles without changing the code logic"),
- new ConventionalCommitType("Tests", "test", "Adding or updating tests"),
- new ConventionalCommitType("Chores", "chore", "Other changes that don't modify src or test files"),
- };
+ public static readonly List Supported = [
+ new("Features", "feat", "Adding a new feature"),
+ new("Bug Fixes", "fix", "Fixing a bug"),
+ new("Work In Progress", "wip", "Still being developed and not yet complete"),
+ new("Reverts", "revert", "Undoing a previous commit"),
+ new("Code Refactoring", "refactor", "Restructuring code without changing its external behavior"),
+ new("Performance Improvements", "perf", "Improves performance"),
+ new("Builds", "build", "Changes that affect the build system or external dependencies"),
+ new("Continuous Integrations", "ci", "Changes to CI configuration files and scripts"),
+ new("Documentations", "docs", "Updating documentation"),
+ new("Styles", "style", "Elements or code styles without changing the code logic"),
+ new("Tests", "test", "Adding or updating tests"),
+ new("Chores", "chore", "Other changes that don't modify src or test files"),
+ ];
public ConventionalCommitType(string name, string type, string description)
{
diff --git a/src/Models/DiffOption.cs b/src/Models/DiffOption.cs
index 98387e7f..5f491d58 100644
--- a/src/Models/DiffOption.cs
+++ b/src/Models/DiffOption.cs
@@ -5,6 +5,15 @@ namespace SourceGit.Models
{
public class DiffOption
{
+ ///
+ /// Enable `--ignore-cr-at-eol` by default?
+ ///
+ public static bool IgnoreCRAtEOL
+ {
+ get;
+ set;
+ } = false;
+
public Change WorkingCopyChange => _workingCopyChange;
public bool IsUnstaged => _isUnstaged;
public List Revisions => _revisions;
@@ -40,7 +49,7 @@ namespace SourceGit.Models
else
{
if (change.DataForAmend != null)
- _extra = "--cached HEAD^";
+ _extra = $"--cached {change.DataForAmend.ParentSHA}";
else
_extra = "--cached";
diff --git a/src/Models/Hyperlink.cs b/src/Models/InlineElement.cs
similarity index 60%
rename from src/Models/Hyperlink.cs
rename to src/Models/InlineElement.cs
index 81dc980e..53761403 100644
--- a/src/Models/Hyperlink.cs
+++ b/src/Models/InlineElement.cs
@@ -1,18 +1,27 @@
namespace SourceGit.Models
{
- public class Hyperlink
+ public enum InlineElementType
{
+ None = 0,
+ Keyword,
+ Link,
+ CommitSHA,
+ Code,
+ }
+
+ public class InlineElement
+ {
+ public InlineElementType Type { get; set; } = InlineElementType.None;
public int Start { get; set; } = 0;
public int Length { get; set; } = 0;
public string Link { get; set; } = "";
- public bool IsCommitSHA { get; set; } = false;
- public Hyperlink(int start, int length, string link, bool isCommitSHA = false)
+ public InlineElement(InlineElementType type, int start, int length, string link)
{
+ Type = type;
Start = start;
Length = length;
Link = link;
- IsCommitSHA = isCommitSHA;
}
public bool Intersect(int start, int length)
diff --git a/src/Models/IpcChannel.cs b/src/Models/IpcChannel.cs
index 2ecfb771..d47a46bd 100644
--- a/src/Models/IpcChannel.cs
+++ b/src/Models/IpcChannel.cs
@@ -22,7 +22,7 @@ namespace SourceGit.Models
_singletoneLock = File.Open(Path.Combine(Native.OS.DataDir, "process.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
_isFirstInstance = true;
_server = new NamedPipeServerStream(
- "SourceGitIPCChannel",
+ "SourceGitIPCChannel" + Environment.UserName,
PipeDirection.In,
-1,
PipeTransmissionMode.Byte,
@@ -40,7 +40,7 @@ namespace SourceGit.Models
{
try
{
- using (var client = new NamedPipeClientStream(".", "SourceGitIPCChannel", PipeDirection.Out))
+ using (var client = new NamedPipeClientStream(".", "SourceGitIPCChannel" + Environment.UserName, PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly))
{
client.Connect(1000);
if (!client.IsConnected)
diff --git a/src/Models/IssueTrackerRule.cs b/src/Models/IssueTrackerRule.cs
index 29487a16..fe0fe8e0 100644
--- a/src/Models/IssueTrackerRule.cs
+++ b/src/Models/IssueTrackerRule.cs
@@ -46,7 +46,7 @@ namespace SourceGit.Models
set => SetProperty(ref _urlTemplate, value);
}
- public void Matches(List outs, string message)
+ public void Matches(List outs, string message)
{
if (_regex == null || string.IsNullOrEmpty(_urlTemplate))
return;
@@ -81,8 +81,7 @@ namespace SourceGit.Models
link = link.Replace($"${j}", group.Value);
}
- var range = new Hyperlink(start, len, link);
- outs.Add(range);
+ outs.Add(new InlineElement(InlineElementType.Link, start, len, link));
}
}
diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs
index 3ccecad4..e930f412 100644
--- a/src/Models/Watcher.cs
+++ b/src/Models/Watcher.cs
@@ -168,6 +168,7 @@ namespace SourceGit.Models
_updateStashes = DateTime.Now.AddSeconds(.5).ToFileTime();
}
else if (name.Equals("HEAD", StringComparison.Ordinal) ||
+ name.Equals("BISECT_START", StringComparison.Ordinal) ||
name.StartsWith("refs/heads/", StringComparison.Ordinal) ||
name.StartsWith("refs/remotes/", StringComparison.Ordinal) ||
(name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal)))
diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml
index 51e3d8bf..9da3a51e 100644
--- a/src/Resources/Icons.axaml
+++ b/src/Resources/Icons.axaml
@@ -2,7 +2,9 @@
M41 512c0-128 46-241 138-333C271 87 384 41 512 41s241 46 333 138c92 92 138 205 138 333s-46 241-138 333c-92 92-205 138-333 138s-241-46-333-138C87 753 41 640 41 512zm87 0c0 108 36 195 113 271s164 113 271 113c108 0 195-36 271-113s113-164 113-271-36-195-113-271c-77-77-164-113-271-113-108 0-195 36-271 113C164 317 128 404 128 512zm256 148V292l195 113L768 512l-195 113-195 113v-77zm148-113-61 36V440l61 36 61 36-61 36z
M304 464a128 128 0 01128-128c71 0 128 57 128 128v224a32 32 0 01-64 0V592h-128v95a32 32 0 01-64 0v-224zm64 1v64h128v-64a64 64 0 00-64-64c-35 0-64 29-64 64zM688 337c18 0 32 14 32 32v319a32 32 0 01-32 32c-18 0-32-14-32-32v-319a32 32 0 0132-32zM84 911l60-143A446 446 0 0164 512C64 265 265 64 512 64s448 201 448 448-201 448-448 448c-54 0-105-9-153-27l-242 22a32 32 0 01-32-44zm133-150-53 126 203-18 13 5c41 15 85 23 131 23 212 0 384-172 384-384S724 128 512 128 128 300 128 512c0 82 26 157 69 220l20 29z
M296 392h64v64h-64zM296 582v160h128V582h-64v-62h-64v62zm80 48v64h-32v-64h32zM360 328h64v64h-64zM296 264h64v64h-64zM360 456h64v64h-64zM360 200h64v64h-64zM855 289 639 73c-6-6-14-9-23-9H192c-18 0-32 14-32 32v832c0 18 14 32 32 32h640c18 0 32-14 32-32V311c0-9-3-17-9-23zM790 326H602V138L790 326zm2 562H232V136h64v64h64v-64h174v216c0 23 19 42 42 42h216v494z
+ M851 755q0 23-16 39l-78 78q-16 16-39 16t-39-16l-168-168-168 168q-16 16-39 16t-39-16l-78-78q-16-16-16-39t16-39l168-168-168-168q-16-16-16-39t16-39l78-78q16-16 39-16t39 16l168 168 168-168q16-16 39-16t39 16l78 78q16 16 16 39t-16 39l-168 168 168 168q16 16 16 39z
M71 1024V0h661L953 219V1024H71zm808-731-220-219H145V951h735V293zM439 512h-220V219h220V512zm-74-219H292v146h74v-146zm0 512h74v73h-220v-73H292v-146H218V585h147v219zm294-366h74V512H512v-73h74v-146H512V219h147v219zm74 439H512V585h220v293zm-74-219h-74v146h74v-146z
+ M128 384a43 43 0 0043-43V213a43 43 0 0143-43h128a43 43 0 000-85H213a128 128 0 00-128 128v128a43 43 0 0043 43zm213 469H213a43 43 0 01-43-43v-128a43 43 0 00-85 0v128a128 128 0 00128 128h128a43 43 0 000-85zm384-299a43 43 0 000-85h-49A171 171 0 00555 347V299a43 43 0 00-85 0v49A171 171 0 00347 469H299a43 43 0 000 85h49A171 171 0 00469 677V725a43 43 0 0085 0v-49A171 171 0 00677 555zm-213 43a85 85 0 1185-85 85 85 0 01-85 85zm384 43a43 43 0 00-43 43v128a43 43 0 01-43 43h-128a43 43 0 000 85h128a128 128 0 00128-128v-128a43 43 0 00-43-43zM811 85h-128a43 43 0 000 85h128a43 43 0 0143 43v128a43 43 0 0085 0V213a128 128 0 00-128-128z
M128 256h192a64 64 0 110 128H128a64 64 0 110-128zm576 192h192a64 64 0 010 128h-192a64 64 0 010-128zm-576 192h192a64 64 0 010 128H128a64 64 0 010-128zm576 0h192a64 64 0 010 128h-192a64 64 0 010-128zm0-384h192a64 64 0 010 128h-192a64 64 0 010-128zM128 448h192a64 64 0 110 128H128a64 64 0 110-128zm384-320a64 64 0 0164 64v640a64 64 0 01-128 0V192a64 64 0 0164-64z
M832 64H192c-18 0-32 14-32 32v832c0 18 14 32 32 32h640c18 0 32-14 32-32V96c0-18-14-32-32-32zM736 596 624 502 506 596V131h230v318z
M509 546 780 275 871 366 509 728 147 366 238 275zM509 728h-362v128h724v-128z
diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml
index 20a70f22..538ac1f1 100644
--- a/src/Resources/Locales/en_US.axaml
+++ b/src/Resources/Locales/en_US.axaml
@@ -36,6 +36,13 @@
NO FILES ASSUMED AS UNCHANGED
REMOVE
BINARY FILE NOT SUPPORTED!!!
+ Bisect
+ Abort
+ Bad
+ Bisecting. Is current HEAD good or bad?
+ Good
+ Skip
+ Bisecting. Mark current commit as good or bad and checkout another one.
Blame
BLAME ON THIS FILE IS NOT SUPPORTED!!!
Checkout ${0}$...
@@ -490,6 +497,7 @@
User Email
Global git user email
Enable --prune on fetch
+ Enable --ignore-cr-at-eol in diff
Git (>= 2.23.0) is required by this app
Install Path
Enable HTTP SSL Verify
diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml
index 204cfc1a..82913d24 100644
--- a/src/Resources/Locales/es_ES.axaml
+++ b/src/Resources/Locales/es_ES.axaml
@@ -40,6 +40,13 @@
NO HAY ARCHIVOS ASUMIDOS COMO SIN CAMBIOS
REMOVER
¡ARCHIVO BINARIO NO SOPORTADO!
+ Bisect
+ Abortar
+ Malo
+ Bisecting. ¿Es el HEAD actual bueno o malo?
+ Bueno
+ Saltar
+ Bisecting. Marcar el commit actual cómo bueno o malo y revisar otro.
Blame
¡BLAME EN ESTE ARCHIVO NO SOPORTADO!
Checkout ${0}$...
@@ -103,8 +110,11 @@
Cherry-Pick ...
Comparar con HEAD
Comparar con Worktree
+ Autor
+ Committer
Información
SHA
+ Asunto
Acción personalizada
Rebase Interactivo ${0}$ hasta Aquí
Merge a ${0}$
@@ -491,6 +501,7 @@
Email de usuario
Email global del usuario git
Habilitar --prune para fetch
+ Habilitar --ignore-cr-at-eol en diff
Se requiere Git (>= 2.23.0) para esta aplicación
Ruta de instalación
Habilitar verificación HTTP SSL
@@ -616,6 +627,7 @@
Ordenar
Abrir en Terminal
Usar tiempo relativo en las historias
+ Ver Logs
WORKTREES
AÑADIR WORKTREE
PRUNE
@@ -701,6 +713,10 @@
Submódulo:
Usar opción --remote
URL:
+ Logs
+ LIMPIAR TODO
+ Copiar
+ Borrar
Advertencia
Página de Bienvenida
Crear Grupo
diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml
index b8c86415..a3fe2b3a 100644
--- a/src/Resources/Locales/ru_RU.axaml
+++ b/src/Resources/Locales/ru_RU.axaml
@@ -14,8 +14,8 @@
Отслеживание ветки:
Отслеживание внешней ветки
Переключиться на:
- создать новую ветку
- ветку из списка
+ Создать новую ветку
+ Ветку из списка
Помощник OpenAI
ПЕРЕСОЗДАТЬ
Использовать OpenAI для создания сообщения о ревизии
@@ -40,9 +40,16 @@
СПИСОК ПУСТ
УДАЛИТЬ
ДВОИЧНЫЙ ФАЙЛ НЕ ПОДДЕРЖИВАЕТСЯ!!!
+ Раздвоить
+ О
+ Плохая
+ Раздвоение. Текущая ГОЛОВА (HEAD) хорошая или плохая?
+ Хорошая
+ Пропустить
+ Раздвоение. Сделать текущую ревизию хорошей или плохой и переключиться на другой.
Расследование
РАССЛЕДОВАНИЕ В ЭТОМ ФАЙЛЕ НЕ ПОДДЕРЖИВАЕТСЯ!!!
- Проверить ${0}$...
+ Переключиться на ${0}$...
Сравнить с ГОЛОВОЙ (HEAD)
Сравнить с рабочим каталогом
Копировать имя ветки
@@ -55,8 +62,8 @@
Поток Git - Завершение ${0}$
Влить ${0}$ в ${1}$...
Влить {0} выделенных веток в текущую
- Забрать ${0}$
- Забрать ${0}$ в ${1}$...
+ Загрузить ${0}$
+ Загрузить ${0}$ в ${1}$...
Выложить ${0}$
Переместить ${0}$ на ${1}$...
Переименовать ${0}$...
@@ -103,8 +110,11 @@
Применить несколько ревизий ...
Сравнить c ГОЛОВОЙ (HEAD)
Сравнить с рабочим каталогом
+ Автор
+ Ревизор
Информацию
SHA
+ Субъект
Пользовательское действие
Интерактивное перемещение (rebase -i) ${0}$ сюда
Влить в ${0}$
@@ -136,6 +146,7 @@
SHA
Открыть в браузере
Описание
+ СУБЪЕКТ
Введите тему ревизии
Настройка репозитория
ШАБЛОН РЕВИЗИИ
@@ -199,7 +210,7 @@
Копировать путь
Создать ветку...
Основан на:
- Проверить созданную ветку
+ Переключиться на созданную ветку
Локальные изменения:
Отклонить
Отложить и применить повторно
@@ -348,9 +359,9 @@
Принудительно разблокировать
Обрезать
Запустить (git lfs prune), чтобы удалить старые файлы LFS из локального хранилища
- Забрать
+ Загрузить
Запустить (git lfs pull), чтобы загрузить все файлы LFS Git для текущей ссылки и проверить
- Забрать объекты LFS
+ Загрузить объекты LFS
Выложить
Отправляйте большие файлы, помещенные в очередь, в конечную точку LFS Git
Выложить объекты LFS
@@ -385,7 +396,7 @@
Извлечение, запускается сразу
Режим доски (по умолчанию)
Режим поиска ревизий
- Забрать, запускается сразу
+ Загрузить, запускается сразу
Выложить, запускается сразу
Принудительно перезагрузить репозиторий
Подготовленные/Неподготовленные выбранные изменения
@@ -490,6 +501,7 @@
Электроная почта пользователя
Общая электроная почта пользователя git
Разрешить (--prune) при скачивании
+ Разрешить (--ignore-cr-at-eol) в различии
Для работы программы требуется версия Git (>= 2.23.0)
Путь установки
Разрешить верификацию HTTP SSL
@@ -512,16 +524,16 @@
Цель:
Удалить рабочий каталог
Информация об обрезке рабочего каталога в «$GIT_COMMON_DIR/worktrees»
- Забрать
+ Загрузить
Ветка внешнего репозитория:
Извлечь все ветки
В:
Локальные изменения:
Отклонить
Отложить и применить повторно
- Забрать без меток
+ Загрузить без меток
Внешний репозиторий:
- Забрать (Получить и слить)
+ Загрузить (Получить и слить)
Использовать перемещение вместо слияния
Выложить
Убедитесь, что подмодули были вставлены
@@ -615,6 +627,7 @@
Сортировать
Открыть в терминале
Использовать относительное время в историях
+ Просмотр логов
РАБОЧИЕ КАТАЛОГИ
ДОБАВИТЬ РАБОЧИЙ КАТАЛОГ
ОБРЕЗАТЬ
@@ -700,6 +713,10 @@
Подмодуль:
Использовать опцию (--remote)
Сетевой адрес:
+ Логи
+ ОЧИСТИТЬ ВСЁ
+ Копировать
+ Удалить
Предупреждение
Приветствие
Создать группу
diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml
index 3d41bccd..39bf38e4 100644
--- a/src/Resources/Locales/zh_CN.axaml
+++ b/src/Resources/Locales/zh_CN.axaml
@@ -40,6 +40,13 @@
没有不跟踪更改的文件
移除
二进制文件不支持该操作!!!
+ 二分定位(bisect)
+ 终止
+ 标记错误
+ 二分定位进行中。当前提交是 '正确' 还是 '错误' ?
+ 标记正确
+ 无法判定
+ 二分定位进行中。请标记当前的提交是 '正确' 还是 '错误',然后检出另一个提交。
逐行追溯(blame)
选中文件不支持该操作!!!
检出(checkout) ${0}$...
@@ -494,6 +501,7 @@
邮箱
默认GIT用户邮箱
拉取更新时启用修剪(--prune)
+ 对比文件时,默认忽略换行符变更 (--ignore-cr-at-eol)
本软件要求GIT最低版本为2.23.0
安装路径
启用HTTP SSL验证
diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml
index 3a10f6ca..181adb69 100644
--- a/src/Resources/Locales/zh_TW.axaml
+++ b/src/Resources/Locales/zh_TW.axaml
@@ -40,6 +40,13 @@
沒有不追蹤變更的檔案
移除
二進位檔案不支援該操作!
+ 二分搜尋 (bisect)
+ 中止
+ 標記為錯誤
+ 二分搜尋進行中。目前的提交是「良好」是「錯誤」?
+ 標記為良好
+ 無法確認
+ 二分搜尋進行中。請標記目前的提交為「良好」或「錯誤」,然後簽出另一個提交。
逐行溯源 (blame)
所選擇的檔案不支援該操作!
簽出 (checkout) ${0}$...
@@ -62,7 +69,7 @@
重新命名 ${0}$...
切換上游分支...
分支比較
- 追蹤上游分支不存在或已刪除!
+ 追蹤上游分支不存在或已刪除!
位元組
取 消
重設檔案到上一版本
@@ -161,7 +168,7 @@
啟用定時自動提取 (fetch) 遠端更新
分鐘
預設遠端存放庫
- 首選合併模式
+ 預設合併模式
Issue 追蹤
新增符合 Azure DevOps 規則
新增符合 Gitee 議題規則
@@ -186,10 +193,10 @@
顏色
名稱
啟動時還原上次開啟的存放庫
- 确认继续
+ 確認繼續
未包含任何檔案變更! 您是否仍要提交 (--allow-empty)?
- 自动暂存并提交
- 未包含任何檔案變更! 您是否仍要提交 (--allow-empty)或者自動暫存全部變更並提交?
+ 自動暫存並提交
+ 未包含任何檔案變更! 您是否仍要提交 (--allow-empty) 或者自動暫存全部變更並提交?
產生約定式提交訊息
破壞性變更:
關閉的 Issue:
@@ -250,7 +257,7 @@
複製
檔案權限已變更
第一個差異
- 忽略空白符號變化和EOL
+ 忽略空白符號變化和 EOL
最後一個差異
LFS 物件變更
下一個差異
@@ -306,8 +313,8 @@
取消暫存
從暫存中移除 {0} 個檔案
取消暫存選取的變更
- 使用我方版本 (checkout --ours)
- 使用對方版本 (checkout --theirs)
+ 使用我方版本 (ours)
+ 使用對方版本 (theirs)
檔案歷史
檔案變更
檔案内容
@@ -465,7 +472,7 @@
啟用串流輸出
外觀設定
預設字型
- 編輯器制表符寬度
+ 編輯器 Tab 寬度
字型大小
預設
程式碼
@@ -494,6 +501,7 @@
電子郵件
預設 Git 使用者電子郵件
拉取變更時進行清理 (--prune)
+ 對比檔案時,預設忽略行尾的 CR 變更 (--ignore-cr-at-eol)
本軟體要求 Git 最低版本為 2.23.0
安裝路徑
啟用 HTTP SSL 驗證
@@ -619,7 +627,7 @@
排序
在終端機中開啟
在提交列表中使用相對時間
- 檢視 GIT 指令的日誌
+ 檢視 Git 指令記錄
工作區列表
新增工作區
清理
@@ -705,8 +713,8 @@
子模組:
啟用 [--remote] 選項
存放庫網址:
- 日誌清單
- 清除所有日誌
+ 記錄
+ 清除所有記錄
複製
刪除
警告
@@ -738,13 +746,13 @@
觸發點擊事件
提交 (修改原始提交)
自動暫存全部變更並提交
- 您已暫存 {0} 檔案,但只顯示 {1} 檔案 ({2} 檔案被篩選器隱藏)。您要繼續嗎?
- 檢測到衝突
+ 您已暫存 {0} 個檔案,但只顯示 {1} 個檔案 ({2} 個檔案被篩選器隱藏)。您確定要繼續提交嗎?
+ 偵測到衝突
使用外部合併工具開啟
使用外部合併工具開啟
檔案衝突已解決
- 使用 MINE
- 使用 THEIRS
+ 使用我方版本 (ours)
+ 使用對方版本 (theirs)
顯示未追蹤檔案
沒有提交訊息記錄
沒有可套用的提交訊息範本
diff --git a/src/Resources/Themes.axaml b/src/Resources/Themes.axaml
index ff4c0374..f33e71d2 100644
--- a/src/Resources/Themes.axaml
+++ b/src/Resources/Themes.axaml
@@ -25,6 +25,7 @@
#A7E1A7
#F19B9D
#0000EE
+ #FFE5E5E5
@@ -51,6 +52,7 @@
#A0308D3C
#A09F4247
#4DAAFC
+ #FF2E2E2E
@@ -79,6 +81,7 @@
+
fonts:Inter#Inter
fonts:SourceGit#JetBrains Mono
diff --git a/src/ViewModels/AIAssistant.cs b/src/ViewModels/AIAssistant.cs
new file mode 100644
index 00000000..8756a30b
--- /dev/null
+++ b/src/ViewModels/AIAssistant.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Avalonia.Threading;
+
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace SourceGit.ViewModels
+{
+ public class AIAssistant : ObservableObject
+ {
+ public bool IsGenerating
+ {
+ get => _isGenerating;
+ private set => SetProperty(ref _isGenerating, value);
+ }
+
+ public string Text
+ {
+ get => _text;
+ private set => SetProperty(ref _text, value);
+ }
+
+ public AIAssistant(Repository repo, Models.OpenAIService service, List changes, Action onApply)
+ {
+ _repo = repo;
+ _service = service;
+ _changes = changes;
+ _onApply = onApply;
+ _cancel = new CancellationTokenSource();
+
+ Gen();
+ }
+
+ public void Regen()
+ {
+ if (_cancel is { IsCancellationRequested: false })
+ _cancel.Cancel();
+
+ Gen();
+ }
+
+ public void Apply()
+ {
+ _onApply?.Invoke(Text);
+ }
+
+ public void Cancel()
+ {
+ _cancel?.Cancel();
+ }
+
+ private void Gen()
+ {
+ Text = string.Empty;
+ IsGenerating = true;
+
+ _cancel = new CancellationTokenSource();
+ Task.Run(() =>
+ {
+ new Commands.GenerateCommitMessage(_service, _repo.FullPath, _changes, _cancel.Token, message =>
+ {
+ Dispatcher.UIThread.Invoke(() => Text = message);
+ }).Exec();
+
+ Dispatcher.UIThread.Invoke(() => IsGenerating = false);
+ }, _cancel.Token);
+ }
+
+ private readonly Repository _repo = null;
+ private Models.OpenAIService _service = null;
+ private List _changes = null;
+ private Action _onApply = null;
+ private CancellationTokenSource _cancel = null;
+ private bool _isGenerating = false;
+ private string _text = string.Empty;
+ }
+}
diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs
index 81c09472..6581d7bb 100644
--- a/src/ViewModels/CommitDetail.cs
+++ b/src/ViewModels/CommitDetail.cs
@@ -134,31 +134,7 @@ namespace SourceGit.ViewModels
public CommitDetail(Repository repo)
{
_repo = repo;
-
- foreach (var remote in repo.Remotes)
- {
- if (remote.TryGetVisitURL(out var url))
- {
- var trimmedUrl = url;
- if (url.EndsWith(".git"))
- trimmedUrl = url.Substring(0, url.Length - 4);
-
- if (url.StartsWith("https://github.com/", StringComparison.Ordinal))
- WebLinks.Add(new Models.CommitLink() { Name = $"Github ({trimmedUrl.Substring(19)})", URLPrefix = $"{url}/commit/" });
- else if (url.StartsWith("https://gitlab.", StringComparison.Ordinal))
- WebLinks.Add(new Models.CommitLink() { Name = $"GitLab ({trimmedUrl.Substring(trimmedUrl.Substring(15).IndexOf('/') + 16)})", URLPrefix = $"{url}/-/commit/" });
- else if (url.StartsWith("https://gitee.com/", StringComparison.Ordinal))
- WebLinks.Add(new Models.CommitLink() { Name = $"Gitee ({trimmedUrl.Substring(18)})", URLPrefix = $"{url}/commit/" });
- else if (url.StartsWith("https://bitbucket.org/", StringComparison.Ordinal))
- WebLinks.Add(new Models.CommitLink() { Name = $"BitBucket ({trimmedUrl.Substring(22)})", URLPrefix = $"{url}/commits/" });
- else if (url.StartsWith("https://codeberg.org/", StringComparison.Ordinal))
- WebLinks.Add(new Models.CommitLink() { Name = $"Codeberg ({trimmedUrl.Substring(21)})", URLPrefix = $"{url}/commit/" });
- else if (url.StartsWith("https://gitea.org/", StringComparison.Ordinal))
- WebLinks.Add(new Models.CommitLink() { Name = $"Gitea ({trimmedUrl.Substring(18)})", URLPrefix = $"{url}/commit/" });
- else if (url.StartsWith("https://git.sr.ht/", StringComparison.Ordinal))
- WebLinks.Add(new Models.CommitLink() { Name = $"sourcehut ({trimmedUrl.Substring(18)})", URLPrefix = $"{url}/commit/" });
- }
- }
+ WebLinks = Models.CommitLink.Get(repo.Remotes);
}
public void Cleanup()
@@ -173,7 +149,6 @@ namespace SourceGit.ViewModels
_diffContext = null;
_viewRevisionFileContent = null;
_cancellationSource = null;
- WebLinks.Clear();
_revisionFiles = null;
_revisionFileSearchSuggestion = null;
}
@@ -332,8 +307,7 @@ namespace SourceGit.ViewModels
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, ev) =>
{
- var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path, _commit.SHA) };
- window.Show();
+ App.ShowWindow(new FileHistories(_repo, change.Path, _commit.SHA), false);
ev.Handled = true;
};
@@ -343,8 +317,7 @@ namespace SourceGit.ViewModels
blame.IsEnabled = change.Index != Models.ChangeState.Deleted;
blame.Click += (_, ev) =>
{
- var window = new Views.Blame() { DataContext = new Blame(_repo.FullPath, change.Path, _commit.SHA) };
- window.Show();
+ App.ShowWindow(new Blame(_repo.FullPath, change.Path, _commit.SHA), false);
ev.Handled = true;
};
@@ -508,8 +481,7 @@ namespace SourceGit.ViewModels
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, ev) =>
{
- var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, file.Path, _commit.SHA) };
- window.Show();
+ App.ShowWindow(new FileHistories(_repo, file.Path, _commit.SHA), false);
ev.Handled = true;
};
@@ -519,8 +491,7 @@ namespace SourceGit.ViewModels
blame.IsEnabled = file.Type == Models.ObjectType.Blob;
blame.Click += (_, ev) =>
{
- var window = new Views.Blame() { DataContext = new Blame(_repo.FullPath, file.Path, _commit.SHA) };
- window.Show();
+ App.ShowWindow(new Blame(_repo.FullPath, file.Path, _commit.SHA), false);
ev.Handled = true;
};
@@ -607,10 +578,10 @@ namespace SourceGit.ViewModels
Task.Run(() =>
{
var message = new Commands.QueryCommitFullMessage(_repo.FullPath, _commit.SHA).Result();
- var links = ParseLinksInMessage(message);
+ var inlines = ParseInlinesInMessage(message);
if (!token.IsCancellationRequested)
- Dispatcher.UIThread.Invoke(() => FullMessage = new Models.CommitFullMessage { Message = message, Links = links });
+ Dispatcher.UIThread.Invoke(() => FullMessage = new Models.CommitFullMessage { Message = message, Inlines = inlines });
});
Task.Run(() =>
@@ -662,13 +633,13 @@ namespace SourceGit.ViewModels
});
}
- private List ParseLinksInMessage(string message)
+ private List ParseInlinesInMessage(string message)
{
- var links = new List();
+ var inlines = new List();
if (_repo.Settings.IssueTrackerRules is { Count: > 0 } rules)
{
foreach (var rule in rules)
- rule.Matches(links, message);
+ rule.Matches(inlines, message);
}
var matches = REG_SHA_FORMAT().Matches(message);
@@ -681,7 +652,7 @@ namespace SourceGit.ViewModels
var start = match.Index;
var len = match.Length;
var intersect = false;
- foreach (var link in links)
+ foreach (var link in inlines)
{
if (link.Intersect(start, len))
{
@@ -696,13 +667,13 @@ namespace SourceGit.ViewModels
var sha = match.Groups[1].Value;
var isCommitSHA = new Commands.IsCommitSHA(_repo.FullPath, sha).Result();
if (isCommitSHA)
- links.Add(new Models.Hyperlink(start, len, sha, true));
+ inlines.Add(new Models.InlineElement(Models.InlineElementType.CommitSHA, start, len, sha));
}
- if (links.Count > 0)
- links.Sort((l, r) => l.Start - r.Start);
+ if (inlines.Count > 0)
+ inlines.Sort((l, r) => l.Start - r.Start);
- return links;
+ return inlines;
}
private void RefreshVisibleChanges()
diff --git a/src/ViewModels/FileHistories.cs b/src/ViewModels/FileHistories.cs
index 417b816d..9f91205e 100644
--- a/src/ViewModels/FileHistories.cs
+++ b/src/ViewModels/FileHistories.cs
@@ -305,11 +305,23 @@ namespace SourceGit.ViewModels
_repo.NavigateToCommit(commit.SHA);
}
+ public string GetCommitFullMessage(Models.Commit commit)
+ {
+ var sha = commit.SHA;
+ if (_fullCommitMessages.TryGetValue(sha, out var msg))
+ return msg;
+
+ msg = new Commands.QueryCommitFullMessage(_repo.FullPath, sha).Result();
+ _fullCommitMessages[sha] = msg;
+ return msg;
+ }
+
private readonly Repository _repo = null;
private readonly string _file = null;
private bool _isLoading = true;
private bool _prevIsDiffMode = true;
private List _commits = null;
+ private Dictionary _fullCommitMessages = new Dictionary();
private object _viewContent = null;
}
}
diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs
index a4a3c515..555954d9 100644
--- a/src/ViewModels/Histories.cs
+++ b/src/ViewModels/Histories.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.IO;
using System.Text;
using System.Threading.Tasks;
@@ -62,6 +63,12 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _detailContext, value);
}
+ public Models.Bisect Bisect
+ {
+ get => _bisect;
+ private set => SetProperty(ref _bisect, value);
+ }
+
public GridLength LeftArea
{
get => _leftArea;
@@ -111,6 +118,37 @@ namespace SourceGit.ViewModels
_detailContext = null;
}
+ public Models.BisectState UpdateBisectInfo()
+ {
+ var test = Path.Combine(_repo.GitDir, "BISECT_START");
+ if (!File.Exists(test))
+ {
+ Bisect = null;
+ return Models.BisectState.None;
+ }
+
+ var info = new Models.Bisect();
+ var dir = Path.Combine(_repo.GitDir, "refs", "bisect");
+ if (Directory.Exists(dir))
+ {
+ var files = new DirectoryInfo(dir).GetFiles();
+ foreach (var file in files)
+ {
+ if (file.Name.StartsWith("bad"))
+ info.Bads.Add(File.ReadAllText(file.FullName).Trim());
+ else if (file.Name.StartsWith("good"))
+ info.Goods.Add(File.ReadAllText(file.FullName).Trim());
+ }
+ }
+
+ Bisect = info;
+
+ if (info.Bads.Count == 0 || info.Goods.Count == 0)
+ return Models.BisectState.WaitingForRange;
+ else
+ return Models.BisectState.Detecting;
+ }
+
public void NavigateTo(string commitSHA)
{
var commit = _commits.Find(x => x.SHA.StartsWith(commitSHA, StringComparison.Ordinal));
@@ -304,7 +342,7 @@ namespace SourceGit.ViewModels
if (picker.Count == 1)
{
log = _repo.CreateLog("Save as Patch");
-
+
var succ = false;
for (var i = 0; i < selected.Count; i++)
{
@@ -570,11 +608,7 @@ namespace SourceGit.ViewModels
return;
}
- App.OpenDialog(new Views.InteractiveRebase()
- {
- DataContext = new InteractiveRebase(_repo, current, commit)
- });
-
+ App.ShowWindow(new InteractiveRebase(_repo, current, commit), true);
e.Handled = true;
};
@@ -1213,7 +1247,7 @@ namespace SourceGit.ViewModels
}
builder.Append(".patch");
- return System.IO.Path.Combine(dir, builder.ToString());
+ return Path.Combine(dir, builder.ToString());
}
private Repository _repo = null;
@@ -1224,6 +1258,8 @@ namespace SourceGit.ViewModels
private long _navigationId = 0;
private object _detailContext = null;
+ private Models.Bisect _bisect = null;
+
private GridLength _leftArea = new GridLength(1, GridUnitType.Star);
private GridLength _rightArea = new GridLength(1, GridUnitType.Star);
private GridLength _topArea = new GridLength(1, GridUnitType.Star);
diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs
index 9ae99b33..84ba96e4 100644
--- a/src/ViewModels/Launcher.cs
+++ b/src/ViewModels/Launcher.cs
@@ -380,7 +380,7 @@ namespace SourceGit.ViewModels
configure.Header = App.Text("Workspace.Configure");
configure.Click += (_, e) =>
{
- App.OpenDialog(new Views.ConfigureWorkspace() { DataContext = new ConfigureWorkspace() });
+ App.ShowWindow(new ConfigureWorkspace(), true);
e.Handled = true;
};
menu.Items.Add(configure);
diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs
index a1830d07..d1e13f38 100644
--- a/src/ViewModels/Preferences.cs
+++ b/src/ViewModels/Preferences.cs
@@ -212,6 +212,19 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _useSyntaxHighlighting, value);
}
+ public bool IgnoreCRAtEOLInDiff
+ {
+ get => Models.DiffOption.IgnoreCRAtEOL;
+ set
+ {
+ if (Models.DiffOption.IgnoreCRAtEOL != value)
+ {
+ Models.DiffOption.IgnoreCRAtEOL = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
public bool IgnoreWhitespaceChangesInDiff
{
get => _ignoreWhitespaceChangesInDiff;
diff --git a/src/ViewModels/Push.cs b/src/ViewModels/Push.cs
index 5bbb9858..917935b0 100644
--- a/src/ViewModels/Push.cs
+++ b/src/ViewModels/Push.cs
@@ -114,6 +114,9 @@ namespace SourceGit.ViewModels
// Set default selected local branch.
if (localBranch != null)
{
+ if (LocalBranches.Count == 0)
+ LocalBranches.Add(localBranch);
+
_selectedLocalBranch = localBranch;
HasSpecifiedLocalBranch = true;
}
diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs
index 3ce40751..1db1d6f3 100644
--- a/src/ViewModels/Repository.cs
+++ b/src/ViewModels/Repository.cs
@@ -398,6 +398,18 @@ namespace SourceGit.ViewModels
get => _workingCopy?.InProgressContext;
}
+ public Models.BisectState BisectState
+ {
+ get => _bisectState;
+ private set => SetProperty(ref _bisectState, value);
+ }
+
+ public bool IsBisectCommandRunning
+ {
+ get => _isBisectCommandRunning;
+ private set => SetProperty(ref _isBisectCommandRunning, value);
+ }
+
public bool IsAutoFetching
{
get => _isAutoFetching;
@@ -939,6 +951,31 @@ namespace SourceGit.ViewModels
return actions;
}
+ public void Bisect(string subcmd)
+ {
+ IsBisectCommandRunning = true;
+ SetWatcherEnabled(false);
+
+ var log = CreateLog($"Bisect({subcmd})");
+ Task.Run(() =>
+ {
+ var succ = new Commands.Bisect(_fullpath, subcmd).Use(log).Exec();
+ log.Complete();
+
+ Dispatcher.UIThread.Invoke(() =>
+ {
+ if (!succ)
+ App.RaiseException(_fullpath, log.Content.Substring(log.Content.IndexOf('\n')).Trim());
+ else if (log.Content.Contains("is the first bad commit"))
+ App.SendNotification(_fullpath, log.Content.Substring(log.Content.IndexOf('\n')).Trim());
+
+ MarkBranchesDirtyManually();
+ SetWatcherEnabled(true);
+ IsBisectCommandRunning = false;
+ });
+ });
+ }
+
public void RefreshBranches()
{
var branches = new Commands.QueryBranches(_fullpath).Result();
@@ -1023,6 +1060,8 @@ namespace SourceGit.ViewModels
_histories.Commits = commits;
_histories.Graph = graph;
+ BisectState = _histories.UpdateBisectInfo();
+
if (!string.IsNullOrEmpty(_navigateToBranchDelayed))
{
var branch = _branches.Find(x => x.FullName == _navigateToBranchDelayed);
@@ -1405,8 +1444,7 @@ namespace SourceGit.ViewModels
{
locks.Click += (_, e) =>
{
- var dialog = new Views.LFSLocks() { DataContext = new LFSLocks(this, _remotes[0].Name) };
- App.OpenDialog(dialog);
+ App.ShowWindow(new LFSLocks(this, _remotes[0].Name), true);
e.Handled = true;
};
}
@@ -1419,8 +1457,7 @@ namespace SourceGit.ViewModels
lockRemote.Header = remoteName;
lockRemote.Click += (_, e) =>
{
- var dialog = new Views.LFSLocks() { DataContext = new LFSLocks(this, remoteName) };
- App.OpenDialog(dialog);
+ App.ShowWindow(new LFSLocks(this, remoteName), true);
e.Handled = true;
};
locks.Items.Add(lockRemote);
@@ -1706,10 +1743,7 @@ namespace SourceGit.ViewModels
compareWithHead.Icon = App.CreateMenuIcon("Icons.Compare");
compareWithHead.Click += (_, _) =>
{
- App.OpenDialog(new Views.BranchCompare()
- {
- DataContext = new BranchCompare(_fullpath, branch, _currentBranch)
- });
+ App.ShowWindow(new BranchCompare(_fullpath, branch, _currentBranch), false);
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(compareWithHead);
@@ -1989,10 +2023,7 @@ namespace SourceGit.ViewModels
compareWithHead.Icon = App.CreateMenuIcon("Icons.Compare");
compareWithHead.Click += (_, _) =>
{
- App.OpenDialog(new Views.BranchCompare()
- {
- DataContext = new BranchCompare(_fullpath, branch, _currentBranch)
- });
+ App.ShowWindow(new BranchCompare(_fullpath, branch, _currentBranch), false);
};
menu.Items.Add(compareWithHead);
@@ -2635,6 +2666,9 @@ namespace SourceGit.ViewModels
private Timer _autoFetchTimer = null;
private DateTime _lastFetchTime = DateTime.MinValue;
+ private Models.BisectState _bisectState = Models.BisectState.None;
+ private bool _isBisectCommandRunning = false;
+
private string _navigateToBranchDelayed = string.Empty;
}
}
diff --git a/src/ViewModels/ScanRepositories.cs b/src/ViewModels/ScanRepositories.cs
index 322c2cda..307694ea 100644
--- a/src/ViewModels/ScanRepositories.cs
+++ b/src/ViewModels/ScanRepositories.cs
@@ -87,11 +87,8 @@ namespace SourceGit.ViewModels
var subdirs = dir.GetDirectories("*", opts);
foreach (var subdir in subdirs)
{
- if (subdir.Name.Equals("node_modules", StringComparison.Ordinal) ||
- subdir.Name.Equals(".svn", StringComparison.Ordinal) ||
- subdir.Name.Equals(".vs", StringComparison.Ordinal) ||
- subdir.Name.Equals(".vscode", StringComparison.Ordinal) ||
- subdir.Name.Equals(".idea", StringComparison.Ordinal))
+ if (subdir.Name.StartsWith(".", StringComparison.Ordinal) ||
+ subdir.Name.Equals("node_modules", StringComparison.Ordinal))
continue;
CallUIThread(() => ProgressDescription = $"Scanning {subdir.FullName}...");
diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs
index 65759412..4d35b5c2 100644
--- a/src/ViewModels/WorkingCopy.cs
+++ b/src/ViewModels/WorkingCopy.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
@@ -323,10 +323,7 @@ namespace SourceGit.ViewModels
public void OpenAssumeUnchanged()
{
- App.OpenDialog(new Views.AssumeUnchangedManager()
- {
- DataContext = new AssumeUnchangedManager(_repo)
- });
+ App.ShowWindow(new AssumeUnchangedManager(_repo), true);
}
public void StashAll(bool autoStart)
@@ -638,7 +635,7 @@ namespace SourceGit.ViewModels
}
else if (_inProgressContext is RevertInProgress revert)
{
- useTheirs.Header = App.Text("FileCM.ResolveUsing", revert.Head.SHA.Substring(0, 10) + " (revert)");
+ useTheirs.Header = App.Text("FileCM.ResolveUsing", $"{revert.Head.SHA.AsSpan().Slice(0, 10)} (revert)");
useMine.Header = App.Text("FileCM.ResolveUsing", _repo.CurrentBranch.Name);
}
else if (_inProgressContext is MergeInProgress merge)
@@ -726,8 +723,7 @@ namespace SourceGit.ViewModels
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, e) =>
{
- var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path) };
- window.Show();
+ App.ShowWindow(new FileHistories(_repo, change.Path), false);
e.Handled = true;
};
@@ -775,7 +771,7 @@ namespace SourceGit.ViewModels
byExtension.Header = App.Text("WorkingCopy.AddToGitIgnore.Extension", extension);
byExtension.Click += (_, e) =>
{
- Commands.GitIgnore.Add(_repo.FullPath, "*" + extension);
+ Commands.GitIgnore.Add(_repo.FullPath, $"*{extension}");
e.Handled = true;
};
addToIgnore.Items.Add(byExtension);
@@ -786,7 +782,7 @@ namespace SourceGit.ViewModels
byExtensionInSameFolder.Click += (_, e) =>
{
var dir = Path.GetDirectoryName(change.Path).Replace("\\", "/");
- Commands.GitIgnore.Add(_repo.FullPath, dir + "/*" + extension);
+ Commands.GitIgnore.Add(_repo.FullPath, $"{dir}/*{extension}");
e.Handled = true;
};
addToIgnore.Items.Add(byExtensionInSameFolder);
@@ -828,7 +824,7 @@ namespace SourceGit.ViewModels
lfsTrackByExtension.Click += async (_, e) =>
{
var log = _repo.CreateLog("Track LFS");
- var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Track("*" + extension, false, log));
+ var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Track($"*{extension}", false, log));
if (succ)
App.SendNotification(_repo.FullPath, $"Tracking all *{extension} files successfully!");
@@ -997,7 +993,7 @@ namespace SourceGit.ViewModels
}
else if (_inProgressContext is RevertInProgress revert)
{
- useTheirs.Header = App.Text("FileCM.ResolveUsing", revert.Head.SHA.Substring(0, 10) + " (revert)");
+ useTheirs.Header = App.Text("FileCM.ResolveUsing", $"{revert.Head.SHA.AsSpan().Slice(0, 10)} (revert)");
useMine.Header = App.Text("FileCM.ResolveUsing", _repo.CurrentBranch.Name);
}
else if (_inProgressContext is MergeInProgress merge)
@@ -1093,8 +1089,7 @@ namespace SourceGit.ViewModels
{
ai.Click += (_, e) =>
{
- var dialog = new Views.AIAssistant(services[0], _repo.FullPath, this, _selectedStaged);
- App.OpenDialog(dialog);
+ App.ShowWindow(new AIAssistant(_repo, services[0], _selectedStaged, t => CommitMessage = t), true);
e.Handled = true;
};
}
@@ -1108,8 +1103,7 @@ namespace SourceGit.ViewModels
item.Header = service.Name;
item.Click += (_, e) =>
{
- var dialog = new Views.AIAssistant(dup, _repo.FullPath, this, _selectedStaged);
- App.OpenDialog(dialog);
+ App.ShowWindow(new AIAssistant(_repo, dup, _selectedStaged, t => CommitMessage = t), true);
e.Handled = true;
};
@@ -1193,8 +1187,7 @@ namespace SourceGit.ViewModels
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, e) =>
{
- var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path) };
- window.Show();
+ App.ShowWindow(new FileHistories(_repo, change.Path), false);
e.Handled = true;
};
@@ -1424,7 +1417,7 @@ namespace SourceGit.ViewModels
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var prefixLen = home.EndsWith('/') ? home.Length - 1 : home.Length;
if (gitTemplate.StartsWith(home, StringComparison.Ordinal))
- friendlyName = "~" + gitTemplate.Substring(prefixLen);
+ friendlyName = $"~{gitTemplate.AsSpan().Slice(prefixLen)}";
}
var gitTemplateItem = new MenuItem();
@@ -1456,9 +1449,11 @@ namespace SourceGit.ViewModels
{
for (int i = 0; i < historiesCount; i++)
{
- var message = _repo.Settings.CommitMessages[i];
+ var message = _repo.Settings.CommitMessages[i].Trim().ReplaceLineEndings("\n");
+ var subjectEndIdx = message.IndexOf('\n');
+ var subject = subjectEndIdx > 0 ? message.Substring(0, subjectEndIdx) : message;
var item = new MenuItem();
- item.Header = message;
+ item.Header = subject;
item.Icon = App.CreateMenuIcon("Icons.Histories");
item.Click += (_, e) =>
{
@@ -1490,8 +1485,7 @@ namespace SourceGit.ViewModels
if (services.Count == 1)
{
- var dialog = new Views.AIAssistant(services[0], _repo.FullPath, this, _staged);
- App.OpenDialog(dialog);
+ App.ShowWindow(new AIAssistant(_repo, services[0], _staged, t => CommitMessage = t), true);
return null;
}
@@ -1503,8 +1497,7 @@ namespace SourceGit.ViewModels
item.Header = service.Name;
item.Click += (_, e) =>
{
- var dialog = new Views.AIAssistant(dup, _repo.FullPath, this, _staged);
- App.OpenDialog(dialog);
+ App.ShowWindow(new AIAssistant(_repo, dup, _staged, t => CommitMessage = t), true);
e.Handled = true;
};
@@ -1533,7 +1526,10 @@ namespace SourceGit.ViewModels
private List GetStagedChanges()
{
if (_useAmend)
- return new Commands.QueryStagedChangesWithAmend(_repo.FullPath).Result();
+ {
+ var head = new Commands.QuerySingleCommit(_repo.FullPath, "HEAD").Result();
+ return new Commands.QueryStagedChangesWithAmend(_repo.FullPath, head.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{head.SHA}^").Result();
+ }
var rs = new List();
foreach (var c in _cached)
@@ -1705,14 +1701,7 @@ namespace SourceGit.ViewModels
if (!string.IsNullOrEmpty(_filter) && _staged.Count > _visibleStaged.Count && !confirmWithFilter)
{
var confirmMessage = App.Text("WorkingCopy.ConfirmCommitWithFilter", _staged.Count, _visibleStaged.Count, _staged.Count - _visibleStaged.Count);
- App.OpenDialog(new Views.ConfirmCommit()
- {
- DataContext = new ConfirmCommit(confirmMessage, () =>
- {
- DoCommit(autoStage, autoPush, allowEmpty, true);
- })
- });
-
+ App.ShowWindow(new ConfirmCommit(confirmMessage, () => DoCommit(autoStage, autoPush, allowEmpty, true)), true);
return;
}
@@ -1720,14 +1709,7 @@ namespace SourceGit.ViewModels
{
if ((autoStage && _count == 0) || (!autoStage && _staged.Count == 0))
{
- App.OpenDialog(new Views.ConfirmEmptyCommit()
- {
- DataContext = new ConfirmEmptyCommit(_count > 0, stageAll =>
- {
- DoCommit(stageAll, autoPush, true, confirmWithFilter);
- })
- });
-
+ App.ShowWindow(new ConfirmEmptyCommit(_count > 0, stageAll => DoCommit(stageAll, autoPush, true, confirmWithFilter)), true);
return;
}
}
@@ -1756,7 +1738,18 @@ namespace SourceGit.ViewModels
UseAmend = false;
if (autoPush && _repo.Remotes.Count > 0)
- _repo.ShowAndStartPopup(new Push(_repo, null));
+ {
+ if (_repo.CurrentBranch == null)
+ {
+ var currentBranchName = Commands.Branch.ShowCurrent(_repo.FullPath);
+ var tmp = new Models.Branch() { Name = currentBranchName };
+ _repo.ShowAndStartPopup(new Push(_repo, tmp));
+ }
+ else
+ {
+ _repo.ShowAndStartPopup(new Push(_repo, null));
+ }
+ }
}
_repo.MarkBranchesDirtyManually();
diff --git a/src/Views/AIAssistant.axaml b/src/Views/AIAssistant.axaml
index e07c3a3e..c9a37f3b 100644
--- a/src/Views/AIAssistant.axaml
+++ b/src/Views/AIAssistant.axaml
@@ -7,6 +7,7 @@
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="120"
x:Class="SourceGit.Views.AIAssistant"
+ x:DataType="vm:AIAssistant"
x:Name="ThisControl"
Icon="/App.ico"
Title="{DynamicResource Text.AIAssistant}"
@@ -48,20 +49,22 @@
-
+
+ IsEnabled="{Binding !IsGenerating}"
+ Click="OnApply"/>
+ IsEnabled="{Binding !IsGenerating}"
+ Command="{Binding Regen}"/>
diff --git a/src/Views/AIAssistant.axaml.cs b/src/Views/AIAssistant.axaml.cs
index 73fea708..f8519b7c 100644
--- a/src/Views/AIAssistant.axaml.cs
+++ b/src/Views/AIAssistant.axaml.cs
@@ -1,13 +1,11 @@
using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
+
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Media;
-using Avalonia.Threading;
+
using AvaloniaEdit;
using AvaloniaEdit.Document;
using AvaloniaEdit.Editing;
@@ -101,76 +99,16 @@ namespace SourceGit.Views
InitializeComponent();
}
- public AIAssistant(Models.OpenAIService service, string repo, ViewModels.WorkingCopy wc, List changes)
- {
- _service = service;
- _repo = repo;
- _wc = wc;
- _changes = changes;
-
- InitializeComponent();
- }
-
- protected override void OnOpened(EventArgs e)
- {
- base.OnOpened(e);
- Generate();
- }
-
protected override void OnClosing(WindowClosingEventArgs e)
{
base.OnClosing(e);
- _cancel?.Cancel();
+ (DataContext as ViewModels.AIAssistant)?.Cancel();
}
- private void OnGenerateCommitMessage(object sender, RoutedEventArgs e)
+ private void OnApply(object sender, RoutedEventArgs e)
{
- if (_wc != null)
- _wc.CommitMessage = TxtResponse.Text;
-
+ (DataContext as ViewModels.AIAssistant)?.Apply();
Close();
}
-
- private void OnRegen(object sender, RoutedEventArgs e)
- {
- TxtResponse.Text = string.Empty;
- Generate();
- e.Handled = true;
- }
-
- private void Generate()
- {
- if (_repo == null)
- return;
-
- IconInProgress.IsVisible = true;
- BtnGenerateCommitMessage.IsEnabled = false;
- BtnRegenerate.IsEnabled = false;
-
- _cancel = new CancellationTokenSource();
- Task.Run(() =>
- {
- new Commands.GenerateCommitMessage(_service, _repo, _changes, _cancel.Token, message =>
- {
- Dispatcher.UIThread.Invoke(() => TxtResponse.Text = message);
- }).Exec();
-
- if (!_cancel.IsCancellationRequested)
- {
- Dispatcher.UIThread.Invoke(() =>
- {
- IconInProgress.IsVisible = false;
- BtnGenerateCommitMessage.IsEnabled = true;
- BtnRegenerate.IsEnabled = true;
- });
- }
- }, _cancel.Token);
- }
-
- private Models.OpenAIService _service;
- private string _repo;
- private ViewModels.WorkingCopy _wc;
- private List _changes;
- private CancellationTokenSource _cancel;
}
}
diff --git a/src/Views/BisectStateIndicator.cs b/src/Views/BisectStateIndicator.cs
new file mode 100644
index 00000000..0a581f53
--- /dev/null
+++ b/src/Views/BisectStateIndicator.cs
@@ -0,0 +1,140 @@
+using System;
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+
+namespace SourceGit.Views
+{
+ public class BisectStateIndicator : Control
+ {
+ public static readonly StyledProperty BackgroundProperty =
+ AvaloniaProperty.Register(nameof(Background), Brushes.Transparent);
+
+ public IBrush Background
+ {
+ get => GetValue(BackgroundProperty);
+ set => SetValue(BackgroundProperty, value);
+ }
+
+ public static readonly StyledProperty ForegroundProperty =
+ AvaloniaProperty.Register(nameof(Foreground), Brushes.White);
+
+ public IBrush Foreground
+ {
+ get => GetValue(ForegroundProperty);
+ set => SetValue(ForegroundProperty, value);
+ }
+
+ public static readonly StyledProperty BisectProperty =
+ AvaloniaProperty.Register(nameof(Bisect));
+
+ public Models.Bisect Bisect
+ {
+ get => GetValue(BisectProperty);
+ set => SetValue(BisectProperty, value);
+ }
+
+ static BisectStateIndicator()
+ {
+ AffectsMeasure(BisectProperty);
+ AffectsRender(BackgroundProperty, ForegroundProperty);
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ if (_flags == Models.BisectCommitFlag.None)
+ return;
+
+ if (_prefix == null)
+ {
+ _prefix = LoadIcon("Icons.Bisect");
+ _good = LoadIcon("Icons.Check");
+ _bad = LoadIcon("Icons.Bad");
+ }
+
+ var x = 0.0;
+
+ if (_flags.HasFlag(Models.BisectCommitFlag.Good))
+ {
+ RenderImpl(context, Brushes.Green, _good, x);
+ x += 36;
+ }
+
+ if (_flags.HasFlag(Models.BisectCommitFlag.Bad))
+ RenderImpl(context, Brushes.Red, _bad, x);
+ }
+
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ base.OnDataContextChanged(e);
+ InvalidateMeasure();
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ var desiredFlags = Models.BisectCommitFlag.None;
+ var desiredWidth = 0.0;
+ if (Bisect is { } bisect && DataContext is Models.Commit commit)
+ {
+ var sha = commit.SHA;
+ if (bisect.Goods.Contains(sha))
+ {
+ desiredFlags |= Models.BisectCommitFlag.Good;
+ desiredWidth = 36;
+ }
+
+ if (bisect.Bads.Contains(sha))
+ {
+ desiredFlags |= Models.BisectCommitFlag.Bad;
+ desiredWidth += 36;
+ }
+ }
+
+ if (desiredFlags != _flags)
+ {
+ _flags = desiredFlags;
+ InvalidateVisual();
+ }
+
+ return new Size(desiredWidth, desiredWidth > 0 ? 16 : 0);
+ }
+
+ private Geometry LoadIcon(string key)
+ {
+ var geo = this.FindResource(key) as StreamGeometry;
+ var drawGeo = geo!.Clone();
+ var iconBounds = drawGeo.Bounds;
+ var translation = Matrix.CreateTranslation(-(Vector)iconBounds.Position);
+ var scale = Math.Min(10.0 / iconBounds.Width, 10.0 / iconBounds.Height);
+ var transform = translation * Matrix.CreateScale(scale, scale);
+ if (drawGeo.Transform == null || drawGeo.Transform.Value == Matrix.Identity)
+ drawGeo.Transform = new MatrixTransform(transform);
+ else
+ drawGeo.Transform = new MatrixTransform(drawGeo.Transform.Value * transform);
+
+ return drawGeo;
+ }
+
+ private void RenderImpl(DrawingContext context, IBrush brush, Geometry icon, double x)
+ {
+ var entireRect = new RoundedRect(new Rect(x, 0, 32, 16), new CornerRadius(2));
+ var stateRect = new RoundedRect(new Rect(x + 16, 0, 16, 16), new CornerRadius(0, 2, 2, 0));
+ context.DrawRectangle(Background, new Pen(brush), entireRect);
+ using (context.PushOpacity(.2))
+ context.DrawRectangle(brush, null, stateRect);
+ context.DrawLine(new Pen(brush), new Point(x + 16, 0), new Point(x + 16, 16));
+
+ using (context.PushTransform(Matrix.CreateTranslation(x + 3, 3)))
+ context.DrawGeometry(Foreground, null, _prefix);
+
+ using (context.PushTransform(Matrix.CreateTranslation(x + 19, 3)))
+ context.DrawGeometry(Foreground, null, icon);
+ }
+
+ private Geometry _prefix = null;
+ private Geometry _good = null;
+ private Geometry _bad = null;
+ private Models.BisectCommitFlag _flags = Models.BisectCommitFlag.None;
+ }
+}
diff --git a/src/Views/BranchTree.axaml b/src/Views/BranchTree.axaml
index 0ac09e6c..c1fecf63 100644
--- a/src/Views/BranchTree.axaml
+++ b/src/Views/BranchTree.axaml
@@ -32,61 +32,63 @@
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
-
+
+
+
+
-
-
+
+
-
-
+
+
+
-
+
diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs
index 51bb01e0..8542157d 100644
--- a/src/Views/BranchTree.axaml.cs
+++ b/src/Views/BranchTree.axaml.cs
@@ -318,6 +318,31 @@ namespace SourceGit.Views
}
}
+ private void OnNodePointerPressed(object sender, PointerPressedEventArgs e)
+ {
+ var p = e.GetCurrentPoint(this);
+ if (!p.Properties.IsLeftButtonPressed)
+ return;
+
+ if (DataContext is not ViewModels.Repository repo)
+ return;
+
+ if (sender is not Border { DataContext: ViewModels.BranchTreeNode node })
+ return;
+
+ if (node.Backend is not Models.Branch branch)
+ return;
+
+ if (BranchesPresenter.SelectedItems is { Count: > 0 })
+ {
+ var ctrl = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control;
+ if (e.KeyModifiers.HasFlag(ctrl) || e.KeyModifiers.HasFlag(KeyModifiers.Shift))
+ return;
+ }
+
+ repo.NavigateToCommit(branch.Head);
+ }
+
private void OnNodesSelectionChanged(object _, SelectionChangedEventArgs e)
{
if (_disableSelectionChangingEvent)
@@ -343,9 +368,6 @@ namespace SourceGit.Views
if (selected == null || selected.Count == 0)
return;
- if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Branch branch })
- repo.NavigateToCommit(branch.Head);
-
var prev = null as ViewModels.BranchTreeNode;
foreach (var row in Rows)
{
diff --git a/src/Views/ChromelessWindow.cs b/src/Views/ChromelessWindow.cs
index 647c657e..dd2485d4 100644
--- a/src/Views/ChromelessWindow.cs
+++ b/src/Views/ChromelessWindow.cs
@@ -18,6 +18,8 @@ namespace SourceGit.Views
public ChromelessWindow()
{
+ Focusable = true;
+
if (OperatingSystem.IsLinux())
{
if (UseSystemWindowFrame)
diff --git a/src/Views/CommitMessagePresenter.cs b/src/Views/CommitMessagePresenter.cs
index c71c9687..0858640b 100644
--- a/src/Views/CommitMessagePresenter.cs
+++ b/src/Views/CommitMessagePresenter.cs
@@ -39,7 +39,7 @@ namespace SourceGit.Views
if (string.IsNullOrEmpty(message))
return;
- var links = FullMessage?.Links;
+ var links = FullMessage?.Inlines;
if (links == null || links.Count == 0)
{
Inlines.Add(new Run(message));
@@ -54,7 +54,7 @@ namespace SourceGit.Views
inlines.Add(new Run(message.Substring(pos, link.Start - pos)));
var run = new Run(message.Substring(link.Start, link.Length));
- run.Classes.Add(link.IsCommitSHA ? "commit_link" : "issue_link");
+ run.Classes.Add(link.Type == Models.InlineElementType.CommitSHA ? "commit_link" : "issue_link");
inlines.Add(run);
pos = link.Start + link.Length;
@@ -87,7 +87,7 @@ namespace SourceGit.Views
scrollViewer.LineDown();
}
}
- else if (FullMessage is { Links: { Count: > 0 } links })
+ else if (FullMessage is { Inlines: { Count: > 0 } links })
{
var point = e.GetPosition(this) - new Point(Padding.Left, Padding.Top);
var x = Math.Min(Math.Max(point.X, 0), Math.Max(TextLayout.WidthIncludingTrailingWhitespace, 0));
@@ -106,7 +106,7 @@ namespace SourceGit.Views
SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
_lastHover = link;
- if (!link.IsCommitSHA)
+ if (link.Type == Models.InlineElementType.Link)
ToolTip.SetTip(this, link.Link);
else
ProcessHoverCommitLink(link);
@@ -127,7 +127,7 @@ namespace SourceGit.Views
var link = _lastHover.Link;
e.Pointer.Capture(null);
- if (_lastHover.IsCommitSHA)
+ if (_lastHover.Type == Models.InlineElementType.CommitSHA)
{
var parentView = this.FindAncestorOfType();
if (parentView is { DataContext: ViewModels.CommitDetail detail })
@@ -252,7 +252,7 @@ namespace SourceGit.Views
ClearHoveredIssueLink();
}
- private void ProcessHoverCommitLink(Models.Hyperlink link)
+ private void ProcessHoverCommitLink(Models.InlineElement link)
{
var sha = link.Link;
@@ -301,7 +301,7 @@ namespace SourceGit.Views
}
}
- private Models.Hyperlink _lastHover = null;
+ private Models.InlineElement _lastHover = null;
private Dictionary _inlineCommits = new();
}
}
diff --git a/src/Views/CommitMessageTextBox.axaml.cs b/src/Views/CommitMessageTextBox.axaml.cs
index 807461d1..83d6f900 100644
--- a/src/Views/CommitMessageTextBox.axaml.cs
+++ b/src/Views/CommitMessageTextBox.axaml.cs
@@ -201,12 +201,7 @@ namespace SourceGit.Views
private void OnOpenConventionalCommitHelper(object _, RoutedEventArgs e)
{
- var dialog = new ConventionalCommitMessageBuilder()
- {
- DataContext = new ViewModels.ConventionalCommitMessageBuilder(text => Text = text)
- };
-
- App.OpenDialog(dialog);
+ App.ShowWindow(new ViewModels.ConventionalCommitMessageBuilder(text => Text = text), true);
e.Handled = true;
}
diff --git a/src/Views/CommitSubjectPresenter.cs b/src/Views/CommitSubjectPresenter.cs
index 83d79fe4..f64323c0 100644
--- a/src/Views/CommitSubjectPresenter.cs
+++ b/src/Views/CommitSubjectPresenter.cs
@@ -1,18 +1,80 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
+using System.Globalization;
using System.Text.RegularExpressions;
+
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
-using Avalonia.Controls.Documents;
using Avalonia.Input;
using Avalonia.Media;
-using Avalonia.Media.TextFormatting;
namespace SourceGit.Views
{
- public partial class CommitSubjectPresenter : TextBlock
+ public partial class CommitSubjectPresenter : Control
{
+ public static readonly StyledProperty FontFamilyProperty =
+ AvaloniaProperty.Register(nameof(FontFamily));
+
+ public FontFamily FontFamily
+ {
+ get => GetValue(FontFamilyProperty);
+ set => SetValue(FontFamilyProperty, value);
+ }
+
+ public static readonly StyledProperty CodeFontFamilyProperty =
+ AvaloniaProperty.Register(nameof(CodeFontFamily));
+
+ public FontFamily CodeFontFamily
+ {
+ get => GetValue(CodeFontFamilyProperty);
+ set => SetValue(CodeFontFamilyProperty, value);
+ }
+
+ public static readonly StyledProperty FontSizeProperty =
+ TextBlock.FontSizeProperty.AddOwner();
+
+ public double FontSize
+ {
+ get => GetValue(FontSizeProperty);
+ set => SetValue(FontSizeProperty, value);
+ }
+
+ public static readonly StyledProperty FontWeightProperty =
+ TextBlock.FontWeightProperty.AddOwner();
+
+ public FontWeight FontWeight
+ {
+ get => GetValue(FontWeightProperty);
+ set => SetValue(FontWeightProperty, value);
+ }
+
+ public static readonly StyledProperty InlineCodeBackgroundProperty =
+ AvaloniaProperty.Register(nameof(InlineCodeBackground), Brushes.Transparent);
+
+ public IBrush InlineCodeBackground
+ {
+ get => GetValue(InlineCodeBackgroundProperty);
+ set => SetValue(InlineCodeBackgroundProperty, value);
+ }
+
+ public static readonly StyledProperty ForegroundProperty =
+ AvaloniaProperty.Register(nameof(Foreground), Brushes.White);
+
+ public IBrush Foreground
+ {
+ get => GetValue(ForegroundProperty);
+ set => SetValue(ForegroundProperty, value);
+ }
+
+ public static readonly StyledProperty LinkForegroundProperty =
+ AvaloniaProperty.Register(nameof(LinkForeground), Brushes.White);
+
+ public IBrush LinkForeground
+ {
+ get => GetValue(LinkForegroundProperty);
+ set => SetValue(LinkForegroundProperty, value);
+ }
+
public static readonly StyledProperty SubjectProperty =
AvaloniaProperty.Register(nameof(Subject));
@@ -31,7 +93,37 @@ namespace SourceGit.Views
set => SetValue(IssueTrackerRulesProperty, value);
}
- protected override Type StyleKeyOverride => typeof(TextBlock);
+ public override void Render(DrawingContext context)
+ {
+ if (_needRebuildInlines)
+ {
+ _needRebuildInlines = false;
+ GenerateFormattedTextElements();
+ }
+
+ if (_inlines.Count == 0)
+ return;
+
+ var height = Bounds.Height;
+ var width = Bounds.Width;
+ foreach (var inline in _inlines)
+ {
+ 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));
+ }
+ }
+ }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
@@ -39,85 +131,69 @@ namespace SourceGit.Views
if (change.Property == SubjectProperty || change.Property == IssueTrackerRulesProperty)
{
- Inlines!.Clear();
- _matches = null;
+ _elements.Clear();
ClearHoveredIssueLink();
var subject = Subject;
if (string.IsNullOrEmpty(subject))
+ {
+ _needRebuildInlines = true;
+ InvalidateVisual();
return;
+ }
var keywordMatch = REG_KEYWORD_FORMAT1().Match(subject);
if (!keywordMatch.Success)
keywordMatch = REG_KEYWORD_FORMAT2().Match(subject);
+ if (keywordMatch.Success)
+ _elements.Add(new Models.InlineElement(Models.InlineElementType.Keyword, 0, keywordMatch.Length, string.Empty));
+
+ var codeMatches = REG_INLINECODE_FORMAT().Matches(subject);
+ for (var i = 0; i < codeMatches.Count; i++)
+ {
+ var match = codeMatches[i];
+ if (!match.Success)
+ continue;
+
+ var start = match.Index;
+ var len = match.Length;
+ var intersect = false;
+ foreach (var exist in _elements)
+ {
+ if (exist.Intersect(start, len))
+ {
+ intersect = true;
+ break;
+ }
+ }
+
+ if (intersect)
+ continue;
+
+ _elements.Add(new Models.InlineElement(Models.InlineElementType.Code, start, len, string.Empty));
+ }
+
var rules = IssueTrackerRules ?? [];
- var matches = new List();
foreach (var rule in rules)
- rule.Matches(matches, subject);
+ rule.Matches(_elements, subject);
- if (matches.Count == 0)
- {
- if (keywordMatch.Success)
- {
- Inlines.Add(new Run(subject.Substring(0, keywordMatch.Length)) { FontWeight = FontWeight.Bold });
- Inlines.Add(new Run(subject.Substring(keywordMatch.Length)));
- }
- else
- {
- Inlines.Add(new Run(subject));
- }
- return;
- }
-
- matches.Sort((l, r) => l.Start - r.Start);
- _matches = matches;
-
- var inlines = new List();
- var pos = 0;
- foreach (var match in matches)
- {
- if (match.Start > pos)
- {
- if (keywordMatch.Success && pos < keywordMatch.Length)
- {
- if (keywordMatch.Length < match.Start)
- {
- inlines.Add(new Run(subject.Substring(pos, keywordMatch.Length - pos)) { FontWeight = FontWeight.Bold });
- inlines.Add(new Run(subject.Substring(keywordMatch.Length, match.Start - keywordMatch.Length)));
- }
- else
- {
- inlines.Add(new Run(subject.Substring(pos, match.Start - pos)) { FontWeight = FontWeight.Bold });
- }
- }
- else
- {
- inlines.Add(new Run(subject.Substring(pos, match.Start - pos)));
- }
- }
-
- var link = new Run(subject.Substring(match.Start, match.Length));
- link.Classes.Add("issue_link");
- inlines.Add(link);
-
- pos = match.Start + match.Length;
- }
-
- if (pos < subject.Length)
- {
- if (keywordMatch.Success && pos < keywordMatch.Length)
- {
- inlines.Add(new Run(subject.Substring(pos, keywordMatch.Length - pos)) { FontWeight = FontWeight.Bold });
- inlines.Add(new Run(subject.Substring(keywordMatch.Length)));
- }
- else
- {
- inlines.Add(new Run(subject.Substring(pos)));
- }
- }
-
- Inlines.AddRange(inlines);
+ _needRebuildInlines = true;
+ InvalidateVisual();
+ }
+ else if (change.Property == FontFamilyProperty ||
+ change.Property == CodeFontFamilyProperty ||
+ change.Property == FontSizeProperty ||
+ change.Property == FontWeightProperty ||
+ change.Property == ForegroundProperty ||
+ change.Property == LinkForegroundProperty)
+ {
+ _needRebuildInlines = true;
+ InvalidateVisual();
+ }
+ else if (change.Property == InlineCodeBackgroundProperty)
+ {
+ InvalidateVisual();
}
}
@@ -125,31 +201,23 @@ namespace SourceGit.Views
{
base.OnPointerMoved(e);
- if (_matches != null)
+ var point = e.GetPosition(this);
+ foreach (var inline in _inlines)
{
- var point = e.GetPosition(this) - new Point(Padding.Left, Padding.Top);
- var x = Math.Min(Math.Max(point.X, 0), Math.Max(TextLayout.WidthIncludingTrailingWhitespace, 0));
- var y = Math.Min(Math.Max(point.Y, 0), Math.Max(TextLayout.Height, 0));
- point = new Point(x, y);
+ if (inline.Element is not { Type: Models.InlineElementType.Link } link)
+ continue;
- var textPosition = TextLayout.HitTestPoint(point).TextPosition;
- foreach (var match in _matches)
- {
- if (!match.Intersect(textPosition, 1))
- continue;
+ if (inline.X > point.X || inline.X + inline.Text.WidthIncludingTrailingWhitespace < point.X)
+ continue;
- if (match == _lastHover)
- return;
-
- _lastHover = match;
- SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
- ToolTip.SetTip(this, match.Link);
- e.Handled = true;
- return;
- }
-
- ClearHoveredIssueLink();
+ _lastHover = link;
+ SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
+ ToolTip.SetTip(this, link.Link);
+ e.Handled = true;
+ return;
}
+
+ ClearHoveredIssueLink();
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
@@ -166,6 +234,94 @@ namespace SourceGit.Views
ClearHoveredIssueLink();
}
+ private void GenerateFormattedTextElements()
+ {
+ _inlines.Clear();
+
+ var subject = Subject;
+ if (string.IsNullOrEmpty(subject))
+ return;
+
+ var fontFamily = FontFamily;
+ var codeFontFamily = CodeFontFamily;
+ var fontSize = FontSize;
+ var foreground = Foreground;
+ var linkForeground = LinkForeground;
+ var typeface = new Typeface(fontFamily, FontStyle.Normal, FontWeight);
+ var codeTypeface = new Typeface(codeFontFamily, FontStyle.Normal, FontWeight);
+ var pos = 0;
+ var x = 0.0;
+ foreach (var elem in _elements)
+ {
+ if (elem.Start > pos)
+ {
+ var normal = new FormattedText(
+ subject.Substring(pos, elem.Start - pos),
+ CultureInfo.CurrentCulture,
+ FlowDirection.LeftToRight,
+ typeface,
+ fontSize,
+ foreground);
+
+ _inlines.Add(new Inline(x, normal, null));
+ x += normal.WidthIncludingTrailingWhitespace;
+ }
+
+ if (elem.Type == Models.InlineElementType.Keyword)
+ {
+ var keyword = new FormattedText(
+ subject.Substring(elem.Start, elem.Length),
+ CultureInfo.CurrentCulture,
+ FlowDirection.LeftToRight,
+ new Typeface(fontFamily, FontStyle.Normal, FontWeight.Bold),
+ fontSize,
+ foreground);
+ _inlines.Add(new Inline(x, keyword, elem));
+ x += keyword.WidthIncludingTrailingWhitespace;
+ }
+ else if (elem.Type == Models.InlineElementType.Link)
+ {
+ var link = new FormattedText(
+ subject.Substring(elem.Start, elem.Length),
+ CultureInfo.CurrentCulture,
+ FlowDirection.LeftToRight,
+ typeface,
+ fontSize,
+ linkForeground);
+ _inlines.Add(new Inline(x, link, elem));
+ x += link.WidthIncludingTrailingWhitespace;
+ }
+ else if (elem.Type == Models.InlineElementType.Code)
+ {
+ var link = new FormattedText(
+ subject.Substring(elem.Start + 1, elem.Length - 2),
+ CultureInfo.CurrentCulture,
+ FlowDirection.LeftToRight,
+ codeTypeface,
+ fontSize,
+ foreground);
+ _inlines.Add(new Inline(x, link, elem));
+ x += link.WidthIncludingTrailingWhitespace + 8;
+ }
+
+ pos = elem.Start + elem.Length;
+ }
+
+ if (pos < subject.Length)
+ {
+ var normal = new FormattedText(
+ subject.Substring(pos),
+ CultureInfo.CurrentCulture,
+ FlowDirection.LeftToRight,
+ typeface,
+ fontSize,
+ foreground);
+
+ _inlines.Add(new Inline(x, normal, null));
+ x += normal.WidthIncludingTrailingWhitespace;
+ }
+ }
+
private void ClearHoveredIssueLink()
{
if (_lastHover != null)
@@ -176,13 +332,32 @@ namespace SourceGit.Views
}
}
+ [GeneratedRegex(@"`.*?`")]
+ private static partial Regex REG_INLINECODE_FORMAT();
+
[GeneratedRegex(@"^\[[\w\s]+\]")]
private static partial Regex REG_KEYWORD_FORMAT1();
[GeneratedRegex(@"^\S+([\<\(][\w\s_\-\*,]+[\>\)])?\!?\s?:\s")]
private static partial Regex REG_KEYWORD_FORMAT2();
- private List _matches = null;
- private Models.Hyperlink _lastHover = null;
+ private class Inline
+ {
+ public double X { get; set; } = 0;
+ public FormattedText Text { get; set; } = null;
+ public Models.InlineElement Element { get; set; } = null;
+
+ public Inline(double x, FormattedText text, Models.InlineElement elem)
+ {
+ X = x;
+ Text = text;
+ Element = elem;
+ }
+ }
+
+ private List _elements = [];
+ private List _inlines = [];
+ private Models.InlineElement _lastHover = null;
+ private bool _needRebuildInlines = false;
}
}
diff --git a/src/Views/FileHistories.axaml b/src/Views/FileHistories.axaml
index e33156fb..e7a5c072 100644
--- a/src/Views/FileHistories.axaml
+++ b/src/Views/FileHistories.axaml
@@ -93,7 +93,9 @@
-
+
+
+
diff --git a/src/Views/FileHistories.axaml.cs b/src/Views/FileHistories.axaml.cs
index be5affc3..3631eb71 100644
--- a/src/Views/FileHistories.axaml.cs
+++ b/src/Views/FileHistories.axaml.cs
@@ -1,3 +1,5 @@
+using System;
+
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
@@ -57,5 +59,22 @@ namespace SourceGit.Views
e.Handled = true;
}
}
+
+ private void OnCommitSubjectDataContextChanged(object sender, EventArgs e)
+ {
+ if (sender is Border border)
+ ToolTip.SetTip(border, null);
+ }
+
+ private void OnCommitSubjectPointerMoved(object sender, PointerEventArgs e)
+ {
+ if (sender is Border { DataContext: Models.Commit commit } border &&
+ DataContext is ViewModels.FileHistories vm)
+ {
+ var tooltip = ToolTip.GetTip(border);
+ if (tooltip == null)
+ ToolTip.SetTip(border, vm.GetCommitFullMessage(commit));
+ }
+ }
}
}
diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml
index 96340d1e..568854fd 100644
--- a/src/Views/Histories.axaml
+++ b/src/Views/Histories.axaml
@@ -126,19 +126,21 @@
-
-
+
+
+
+
-
-
-
+
SetValue(CurrentBranchProperty, value);
}
+ public static readonly StyledProperty BisectProperty =
+ AvaloniaProperty.Register(nameof(Bisect));
+
+ public Models.Bisect Bisect
+ {
+ get => GetValue(BisectProperty);
+ set => SetValue(BisectProperty, value);
+ }
+
public static readonly StyledProperty> IssueTrackerRulesProperty =
AvaloniaProperty.Register>(nameof(IssueTrackerRules));
diff --git a/src/Views/Hotkeys.axaml.cs b/src/Views/Hotkeys.axaml.cs
index d8b5e1a8..ea293b0a 100644
--- a/src/Views/Hotkeys.axaml.cs
+++ b/src/Views/Hotkeys.axaml.cs
@@ -1,3 +1,5 @@
+using Avalonia.Input;
+
namespace SourceGit.Views
{
public partial class Hotkeys : ChromelessWindow
@@ -6,5 +8,13 @@ namespace SourceGit.Views
{
InitializeComponent();
}
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ base.OnKeyDown(e);
+
+ if (!e.Handled && e.Key == Key.Escape)
+ Close();
+ }
}
}
diff --git a/src/Views/Launcher.axaml b/src/Views/Launcher.axaml
index 0909cd08..9876429e 100644
--- a/src/Views/Launcher.axaml
+++ b/src/Views/Launcher.axaml
@@ -45,7 +45,7 @@
-
-
+
+
+
diff --git a/src/Views/RepositoryToolbar.axaml.cs b/src/Views/RepositoryToolbar.axaml.cs
index dbcf73ec..80b5544d 100644
--- a/src/Views/RepositoryToolbar.axaml.cs
+++ b/src/Views/RepositoryToolbar.axaml.cs
@@ -22,22 +22,20 @@ namespace SourceGit.Views
}
}
- private async void OpenStatistics(object _, RoutedEventArgs e)
+ private void OpenStatistics(object _, RoutedEventArgs e)
{
- if (DataContext is ViewModels.Repository repo && TopLevel.GetTopLevel(this) is Window owner)
+ if (DataContext is ViewModels.Repository repo)
{
- var dialog = new Statistics() { DataContext = new ViewModels.Statistics(repo.FullPath) };
- await dialog.ShowDialog(owner);
+ App.ShowWindow(new ViewModels.Statistics(repo.FullPath), true);
e.Handled = true;
}
}
- private async void OpenConfigure(object sender, RoutedEventArgs e)
+ private void OpenConfigure(object sender, RoutedEventArgs e)
{
- if (DataContext is ViewModels.Repository repo && TopLevel.GetTopLevel(this) is Window owner)
+ if (DataContext is ViewModels.Repository repo)
{
- var dialog = new RepositoryConfigure() { DataContext = new ViewModels.RepositoryConfigure(repo) };
- await dialog.ShowDialog(owner);
+ App.ShowWindow(new ViewModels.RepositoryConfigure(repo), true);
e.Handled = true;
}
}
@@ -118,6 +116,21 @@ namespace SourceGit.Views
e.Handled = true;
}
+ private void StartBisect(object sender, RoutedEventArgs e)
+ {
+ if (DataContext is ViewModels.Repository { IsBisectCommandRunning: false } repo &&
+ repo.InProgressContext == null &&
+ repo.CanCreatePopup())
+ {
+ if (repo.LocalChangesCount > 0)
+ App.RaiseException(repo.FullPath, "You have un-committed local changes. Please discard or stash them first.");
+ else
+ repo.Bisect("start");
+ }
+
+ e.Handled = true;
+ }
+
private void OpenCustomActionMenu(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.Repository repo && sender is Control control)
@@ -129,12 +142,11 @@ namespace SourceGit.Views
e.Handled = true;
}
- private async void OpenGitLogs(object sender, RoutedEventArgs e)
+ private void OpenGitLogs(object sender, RoutedEventArgs e)
{
- if (DataContext is ViewModels.Repository repo && TopLevel.GetTopLevel(this) is Window owner)
+ if (DataContext is ViewModels.Repository repo)
{
- var dialog = new ViewLogs() { DataContext = new ViewModels.ViewLogs(repo) };
- await dialog.ShowDialog(owner);
+ App.ShowWindow(new ViewModels.ViewLogs(repo), true);
e.Handled = true;
}
}
diff --git a/src/Views/TagsView.axaml b/src/Views/TagsView.axaml
index b5384c8f..2a575cb3 100644
--- a/src/Views/TagsView.axaml
+++ b/src/Views/TagsView.axaml
@@ -26,36 +26,36 @@
SelectionChanged="OnRowSelectionChanged">
-
-
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -69,23 +69,22 @@
SelectionChanged="OnRowSelectionChanged">
-
-
+
+
+
-
+
-
-
+
+
+
diff --git a/src/Views/TagsView.axaml.cs b/src/Views/TagsView.axaml.cs
index c83cfd28..ba6740c0 100644
--- a/src/Views/TagsView.axaml.cs
+++ b/src/Views/TagsView.axaml.cs
@@ -199,15 +199,27 @@ namespace SourceGit.Views
private void OnDoubleTappedNode(object sender, TappedEventArgs e)
{
- if (sender is Grid { DataContext: ViewModels.TagTreeNode node })
- {
- if (node.IsFolder)
- ToggleNodeIsExpanded(node);
- }
+ if (sender is Control { DataContext: ViewModels.TagTreeNode { IsFolder: true } node })
+ ToggleNodeIsExpanded(node);
e.Handled = true;
}
+ private void OnRowPointerPressed(object sender, PointerPressedEventArgs e)
+ {
+ var p = e.GetCurrentPoint(this);
+ if (!p.Properties.IsLeftButtonPressed)
+ return;
+
+ if (DataContext is not ViewModels.Repository repo)
+ return;
+
+ if (sender is Control { DataContext: Models.Tag tag })
+ repo.NavigateToCommit(tag.SHA);
+ else if (sender is Control { DataContext: ViewModels.TagTreeNode { Tag: { } nodeTag } })
+ repo.NavigateToCommit(nodeTag.SHA);
+ }
+
private void OnRowContextRequested(object sender, ContextRequestedEventArgs e)
{
var control = sender as Control;
@@ -240,11 +252,8 @@ namespace SourceGit.Views
else if (selected is Models.Tag tag)
selectedTag = tag;
- if (selectedTag != null && DataContext is ViewModels.Repository repo)
- {
+ if (selectedTag != null)
RaiseEvent(new RoutedEventArgs(SelectionChangedEvent));
- repo.NavigateToCommit(selectedTag.SHA);
- }
}
private void MakeTreeRows(List rows, List nodes)
diff --git a/src/Views/ViewLogs.axaml b/src/Views/ViewLogs.axaml
index e29a5afd..ccdd1f4d 100644
--- a/src/Views/ViewLogs.axaml
+++ b/src/Views/ViewLogs.axaml
@@ -96,6 +96,13 @@
+
+