Merge branch 'release/v2025.15'

This commit is contained in:
leo 2025-04-28 09:16:44 +08:00
commit 92f215d039
No known key found for this signature in database
64 changed files with 1364 additions and 560 deletions

View file

@ -39,6 +39,7 @@
* Search commits
* GitFlow
* Git LFS
* Bisect
* Issue Link
* Workspace
* Custom Action

View file

@ -6,11 +6,18 @@ This document shows the translation status of each locale file in the repository
### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)
### ![de__DE](https://img.shields.io/badge/de__DE-96.19%25-yellow)
### ![de__DE](https://img.shields.io/badge/de__DE-95.19%25-yellow)
<details>
<summary>Missing keys in de_DE.axaml</summary>
- 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
</details>
### ![es__ES](https://img.shields.io/badge/es__ES-98.95%25-yellow)
### ![es__ES](https://img.shields.io/badge/es__ES-%E2%88%9A-brightgreen)
<details>
<summary>Missing keys in es_ES.axaml</summary>
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.Repository.ViewLogs
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
</details>
### ![fr__FR](https://img.shields.io/badge/fr__FR-97.51%25-yellow)
### ![fr__FR](https://img.shields.io/badge/fr__FR-96.49%25-yellow)
<details>
<summary>Missing keys in fr_FR.axaml</summary>
- 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
</details>
### ![it__IT](https://img.shields.io/badge/it__IT-97.24%25-yellow)
### ![it__IT](https://img.shields.io/badge/it__IT-96.23%25-yellow)
<details>
<summary>Missing keys in it_IT.axaml</summary>
- 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
</details>
### ![ja__JP](https://img.shields.io/badge/ja__JP-97.24%25-yellow)
### ![ja__JP](https://img.shields.io/badge/ja__JP-96.23%25-yellow)
<details>
<summary>Missing keys in ja_JP.axaml</summary>
- 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
</details>
### ![pt__BR](https://img.shields.io/badge/pt__BR-88.71%25-yellow)
### ![pt__BR](https://img.shields.io/badge/pt__BR-87.79%25-yellow)
<details>
<summary>Missing keys in pt_BR.axaml</summary>
@ -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
</details>
### ![ru__RU](https://img.shields.io/badge/ru__RU-98.82%25-yellow)
### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen)
<details>
<summary>Missing keys in ru_RU.axaml</summary>
- 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
</details>
### ![ta__IN](https://img.shields.io/badge/ta__IN-97.51%25-yellow)
### ![ta__IN](https://img.shields.io/badge/ta__IN-96.49%25-yellow)
<details>
<summary>Missing keys in ta_IN.axaml</summary>
- 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
</details>
### ![uk__UA](https://img.shields.io/badge/uk__UA-98.69%25-yellow)
### ![uk__UA](https://img.shields.io/badge/uk__UA-97.66%25-yellow)
<details>
<summary>Missing keys in uk_UA.axaml</summary>
- 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

View file

@ -1 +1 @@
2025.14
2025.15

View file

@ -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 =>

View file

@ -35,7 +35,7 @@
<NativeMenu.Menu>
<NativeMenu>
<NativeMenuItem Header="{DynamicResource Text.About.Menu}" Command="{x:Static s:App.OpenAboutCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}" Gesture="F1"/>
<NativeMenuItem Header="{DynamicResource Text.SelfUpdate}" Command="{x:Static s:App.CheckForUpdateCommand}" IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="{DynamicResource Text.Preferences}" Command="{x:Static s:App.OpenPreferencesCommand}" Gesture="⌘+,"/>

View file

@ -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 })
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<Control>((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);
});
}

13
src/Commands/Bisect.cs Normal file
View file

@ -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}";
}
}
}

View file

@ -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;
}

View file

@ -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<Models.Change> 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;
}
}

35
src/Models/Bisect.cs Normal file
View file

@ -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<string> Bads
{
get;
set;
} = [];
public HashSet<string> Goods
{
get;
set;
} = [];
}
}

View file

@ -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

View file

@ -117,6 +117,6 @@ namespace SourceGit.Models
public class CommitFullMessage
{
public string Message { get; set; } = string.Empty;
public List<Hyperlink> Links { get; set; } = [];
public List<InlineElement> Inlines { get; set; } = [];
}
}

View file

@ -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<CommitLink> Get(List<Remote> remotes)
{
var outs = new List<CommitLink>();
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;
}
}
}

View file

@ -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<ConventionalCommitType> Supported = new List<ConventionalCommitType>()
{
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<ConventionalCommitType> 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)
{

View file

@ -5,6 +5,15 @@ namespace SourceGit.Models
{
public class DiffOption
{
/// <summary>
/// Enable `--ignore-cr-at-eol` by default?
/// </summary>
public static bool IgnoreCRAtEOL
{
get;
set;
} = false;
public Change WorkingCopyChange => _workingCopyChange;
public bool IsUnstaged => _isUnstaged;
public List<string> 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";

View file

@ -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)

View file

@ -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)

View file

@ -46,7 +46,7 @@ namespace SourceGit.Models
set => SetProperty(ref _urlTemplate, value);
}
public void Matches(List<Hyperlink> outs, string message)
public void Matches(List<InlineElement> 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));
}
}

View file

@ -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)))

View file

@ -2,7 +2,9 @@
<StreamGeometry x:Key="Icons.Action">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</StreamGeometry>
<StreamGeometry x:Key="Icons.AIAssist">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</StreamGeometry>
<StreamGeometry x:Key="Icons.Archive">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</StreamGeometry>
<StreamGeometry x:Key="Icons.Bad">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</StreamGeometry>
<StreamGeometry x:Key="Icons.Binary">M71 1024V0h661L953 219V1024H71zm808-731-220-219H145V951h735V293zM439 512h-220V219h220V512zm-74-219H292v146h74v-146zm0 512h74v73h-220v-73H292v-146H218V585h147v219zm294-366h74V512H512v-73h74v-146H512V219h147v219zm74 439H512V585h220v293zm-74-219h-74v146h74v-146z</StreamGeometry>
<StreamGeometry x:Key="Icons.Bisect">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</StreamGeometry>
<StreamGeometry x:Key="Icons.Blame">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</StreamGeometry>
<StreamGeometry x:Key="Icons.Bookmark">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</StreamGeometry>
<StreamGeometry x:Key="Icons.Bottom">M509 546 780 275 871 366 509 728 147 366 238 275zM509 728h-362v128h724v-128z</StreamGeometry>

View file

@ -36,6 +36,13 @@
<x:String x:Key="Text.AssumeUnchanged.Empty" xml:space="preserve">NO FILES ASSUMED AS UNCHANGED</x:String>
<x:String x:Key="Text.AssumeUnchanged.Remove" xml:space="preserve">REMOVE</x:String>
<x:String x:Key="Text.BinaryNotSupported" xml:space="preserve">BINARY FILE NOT SUPPORTED!!!</x:String>
<x:String x:Key="Text.Bisect">Bisect</x:String>
<x:String x:Key="Text.Bisect.Abort">Abort</x:String>
<x:String x:Key="Text.Bisect.Bad">Bad</x:String>
<x:String x:Key="Text.Bisect.Detecting">Bisecting. Is current HEAD good or bad?</x:String>
<x:String x:Key="Text.Bisect.Good">Good</x:String>
<x:String x:Key="Text.Bisect.Skip">Skip</x:String>
<x:String x:Key="Text.Bisect.WaitingForRange">Bisecting. Mark current commit as good or bad and checkout another one.</x:String>
<x:String x:Key="Text.Blame" xml:space="preserve">Blame</x:String>
<x:String x:Key="Text.BlameTypeNotSupported" xml:space="preserve">BLAME ON THIS FILE IS NOT SUPPORTED!!!</x:String>
<x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">Checkout ${0}$...</x:String>
@ -490,6 +497,7 @@
<x:String x:Key="Text.Preferences.Git.Email" xml:space="preserve">User Email</x:String>
<x:String x:Key="Text.Preferences.Git.Email.Placeholder" xml:space="preserve">Global git user email</x:String>
<x:String x:Key="Text.Preferences.Git.EnablePruneOnFetch" xml:space="preserve">Enable --prune on fetch</x:String>
<x:String x:Key="Text.Preferences.Git.IgnoreCRAtEOLInDiff" xml:space="preserve">Enable --ignore-cr-at-eol in diff</x:String>
<x:String x:Key="Text.Preferences.Git.Invalid" xml:space="preserve">Git (&gt;= 2.23.0) is required by this app</x:String>
<x:String x:Key="Text.Preferences.Git.Path" xml:space="preserve">Install Path</x:String>
<x:String x:Key="Text.Preferences.Git.SSLVerify" xml:space="preserve">Enable HTTP SSL Verify</x:String>

View file

@ -40,6 +40,13 @@
<x:String x:Key="Text.AssumeUnchanged.Empty" xml:space="preserve">NO HAY ARCHIVOS ASUMIDOS COMO SIN CAMBIOS</x:String>
<x:String x:Key="Text.AssumeUnchanged.Remove" xml:space="preserve">REMOVER</x:String>
<x:String x:Key="Text.BinaryNotSupported" xml:space="preserve">¡ARCHIVO BINARIO NO SOPORTADO!</x:String>
<x:String x:Key="Text.Bisect">Bisect</x:String>
<x:String x:Key="Text.Bisect.Abort">Abortar</x:String>
<x:String x:Key="Text.Bisect.Bad">Malo</x:String>
<x:String x:Key="Text.Bisect.Detecting">Bisecting. ¿Es el HEAD actual bueno o malo?</x:String>
<x:String x:Key="Text.Bisect.Good">Bueno</x:String>
<x:String x:Key="Text.Bisect.Skip">Saltar</x:String>
<x:String x:Key="Text.Bisect.WaitingForRange">Bisecting. Marcar el commit actual cómo bueno o malo y revisar otro.</x:String>
<x:String x:Key="Text.Blame" xml:space="preserve">Blame</x:String>
<x:String x:Key="Text.BlameTypeNotSupported" xml:space="preserve">¡BLAME EN ESTE ARCHIVO NO SOPORTADO!</x:String>
<x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">Checkout ${0}$...</x:String>
@ -103,8 +110,11 @@
<x:String x:Key="Text.CommitCM.CherryPickMultiple" xml:space="preserve">Cherry-Pick ...</x:String>
<x:String x:Key="Text.CommitCM.CompareWithHead" xml:space="preserve">Comparar con HEAD</x:String>
<x:String x:Key="Text.CommitCM.CompareWithWorktree" xml:space="preserve">Comparar con Worktree</x:String>
<x:String x:Key="Text.CommitCM.CopyAuthor" xml:space="preserve">Autor</x:String>
<x:String x:Key="Text.CommitCM.CopyCommitter" xml:space="preserve">Committer</x:String>
<x:String x:Key="Text.CommitCM.CopyInfo" xml:space="preserve">Información</x:String>
<x:String x:Key="Text.CommitCM.CopySHA" xml:space="preserve">SHA</x:String>
<x:String x:Key="Text.CommitCM.CopySubject" xml:space="preserve">Asunto</x:String>
<x:String x:Key="Text.CommitCM.CustomAction" xml:space="preserve">Acción personalizada</x:String>
<x:String x:Key="Text.CommitCM.InteractiveRebase" xml:space="preserve">Rebase Interactivo ${0}$ hasta Aquí</x:String>
<x:String x:Key="Text.CommitCM.Merge" xml:space="preserve">Merge a ${0}$</x:String>
@ -491,6 +501,7 @@
<x:String x:Key="Text.Preferences.Git.Email" xml:space="preserve">Email de usuario</x:String>
<x:String x:Key="Text.Preferences.Git.Email.Placeholder" xml:space="preserve">Email global del usuario git</x:String>
<x:String x:Key="Text.Preferences.Git.EnablePruneOnFetch" xml:space="preserve">Habilitar --prune para fetch</x:String>
<x:String x:Key="Text.Preferences.Git.IgnoreCRAtEOLInDiff" xml:space="preserve">Habilitar --ignore-cr-at-eol en diff</x:String>
<x:String x:Key="Text.Preferences.Git.Invalid" xml:space="preserve">Se requiere Git (&gt;= 2.23.0) para esta aplicación</x:String>
<x:String x:Key="Text.Preferences.Git.Path" xml:space="preserve">Ruta de instalación</x:String>
<x:String x:Key="Text.Preferences.Git.SSLVerify" xml:space="preserve">Habilitar verificación HTTP SSL</x:String>
@ -616,6 +627,7 @@
<x:String x:Key="Text.Repository.Tags.Sort" xml:space="preserve">Ordenar</x:String>
<x:String x:Key="Text.Repository.Terminal" xml:space="preserve">Abrir en Terminal</x:String>
<x:String x:Key="Text.Repository.UseRelativeTimeInHistories" xml:space="preserve">Usar tiempo relativo en las historias</x:String>
<x:String x:Key="Text.Repository.ViewLogs" xml:space="preserve">Ver Logs</x:String>
<x:String x:Key="Text.Repository.Worktrees" xml:space="preserve">WORKTREES</x:String>
<x:String x:Key="Text.Repository.Worktrees.Add" xml:space="preserve">AÑADIR WORKTREE</x:String>
<x:String x:Key="Text.Repository.Worktrees.Prune" xml:space="preserve">PRUNE</x:String>
@ -701,6 +713,10 @@
<x:String x:Key="Text.UpdateSubmodules.Target" xml:space="preserve">Submódulo:</x:String>
<x:String x:Key="Text.UpdateSubmodules.UseRemote" xml:space="preserve">Usar opción --remote</x:String>
<x:String x:Key="Text.URL" xml:space="preserve">URL:</x:String>
<x:String x:Key="Text.ViewLogs" xml:space="preserve">Logs</x:String>
<x:String x:Key="Text.ViewLogs.Clear" xml:space="preserve">LIMPIAR TODO</x:String>
<x:String x:Key="Text.ViewLogs.CopyLog" xml:space="preserve">Copiar</x:String>
<x:String x:Key="Text.ViewLogs.Delete" xml:space="preserve">Borrar</x:String>
<x:String x:Key="Text.Warn" xml:space="preserve">Advertencia</x:String>
<x:String x:Key="Text.Welcome" xml:space="preserve">Página de Bienvenida</x:String>
<x:String x:Key="Text.Welcome.AddRootFolder" xml:space="preserve">Crear Grupo</x:String>

View file

@ -14,8 +14,8 @@
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">Отслеживание ветки:</x:String>
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">Отслеживание внешней ветки</x:String>
<x:String x:Key="Text.AddWorktree.WhatToCheckout" xml:space="preserve">Переключиться на:</x:String>
<x:String x:Key="Text.AddWorktree.WhatToCheckout.CreateNew" xml:space="preserve">создать новую ветку</x:String>
<x:String x:Key="Text.AddWorktree.WhatToCheckout.Existing" xml:space="preserve">ветку из списка</x:String>
<x:String x:Key="Text.AddWorktree.WhatToCheckout.CreateNew" xml:space="preserve">Создать новую ветку</x:String>
<x:String x:Key="Text.AddWorktree.WhatToCheckout.Existing" xml:space="preserve">Ветку из списка</x:String>
<x:String x:Key="Text.AIAssistant" xml:space="preserve">Помощник OpenAI</x:String>
<x:String x:Key="Text.AIAssistant.Regen" xml:space="preserve">ПЕРЕСОЗДАТЬ</x:String>
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">Использовать OpenAI для создания сообщения о ревизии</x:String>
@ -40,9 +40,16 @@
<x:String x:Key="Text.AssumeUnchanged.Empty" xml:space="preserve">СПИСОК ПУСТ</x:String>
<x:String x:Key="Text.AssumeUnchanged.Remove" xml:space="preserve">УДАЛИТЬ</x:String>
<x:String x:Key="Text.BinaryNotSupported" xml:space="preserve">ДВОИЧНЫЙ ФАЙЛ НЕ ПОДДЕРЖИВАЕТСЯ!!!</x:String>
<x:String x:Key="Text.Bisect">Раздвоить</x:String>
<x:String x:Key="Text.Bisect.Abort">О</x:String>
<x:String x:Key="Text.Bisect.Bad">Плохая</x:String>
<x:String x:Key="Text.Bisect.Detecting">Раздвоение. Текущая ГОЛОВА (HEAD) хорошая или плохая?</x:String>
<x:String x:Key="Text.Bisect.Good">Хорошая</x:String>
<x:String x:Key="Text.Bisect.Skip">Пропустить</x:String>
<x:String x:Key="Text.Bisect.WaitingForRange">Раздвоение. Сделать текущую ревизию хорошей или плохой и переключиться на другой.</x:String>
<x:String x:Key="Text.Blame" xml:space="preserve">Расследование</x:String>
<x:String x:Key="Text.BlameTypeNotSupported" xml:space="preserve">РАССЛЕДОВАНИЕ В ЭТОМ ФАЙЛЕ НЕ ПОДДЕРЖИВАЕТСЯ!!!</x:String>
<x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">Проверить ${0}$...</x:String>
<x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">Переключиться на ${0}$...</x:String>
<x:String x:Key="Text.BranchCM.CompareWithHead" xml:space="preserve">Сравнить с ГОЛОВОЙ (HEAD)</x:String>
<x:String x:Key="Text.BranchCM.CompareWithWorktree" xml:space="preserve">Сравнить с рабочим каталогом</x:String>
<x:String x:Key="Text.BranchCM.CopyName" xml:space="preserve">Копировать имя ветки</x:String>
@ -55,8 +62,8 @@
<x:String x:Key="Text.BranchCM.Finish" xml:space="preserve">Поток Git - Завершение ${0}$</x:String>
<x:String x:Key="Text.BranchCM.Merge" xml:space="preserve">Влить ${0}$ в ${1}$...</x:String>
<x:String x:Key="Text.BranchCM.MergeMultiBranches" xml:space="preserve">Влить {0} выделенных веток в текущую</x:String>
<x:String x:Key="Text.BranchCM.Pull" xml:space="preserve">Забрать ${0}$</x:String>
<x:String x:Key="Text.BranchCM.PullInto" xml:space="preserve">Забрать ${0}$ в ${1}$...</x:String>
<x:String x:Key="Text.BranchCM.Pull" xml:space="preserve">Загрузить ${0}$</x:String>
<x:String x:Key="Text.BranchCM.PullInto" xml:space="preserve">Загрузить ${0}$ в ${1}$...</x:String>
<x:String x:Key="Text.BranchCM.Push" xml:space="preserve">Выложить ${0}$</x:String>
<x:String x:Key="Text.BranchCM.Rebase" xml:space="preserve">Переместить ${0}$ на ${1}$...</x:String>
<x:String x:Key="Text.BranchCM.Rename" xml:space="preserve">Переименовать ${0}$...</x:String>
@ -103,8 +110,11 @@
<x:String x:Key="Text.CommitCM.CherryPickMultiple" xml:space="preserve">Применить несколько ревизий ...</x:String>
<x:String x:Key="Text.CommitCM.CompareWithHead" xml:space="preserve">Сравнить c ГОЛОВОЙ (HEAD)</x:String>
<x:String x:Key="Text.CommitCM.CompareWithWorktree" xml:space="preserve">Сравнить с рабочим каталогом</x:String>
<x:String x:Key="Text.CommitCM.CopyAuthor" xml:space="preserve">Автор</x:String>
<x:String x:Key="Text.CommitCM.CopyCommitter" xml:space="preserve">Ревизор</x:String>
<x:String x:Key="Text.CommitCM.CopyInfo" xml:space="preserve">Информацию</x:String>
<x:String x:Key="Text.CommitCM.CopySHA" xml:space="preserve">SHA</x:String>
<x:String x:Key="Text.CommitCM.CopySubject" xml:space="preserve">Субъект</x:String>
<x:String x:Key="Text.CommitCM.CustomAction" xml:space="preserve">Пользовательское действие</x:String>
<x:String x:Key="Text.CommitCM.InteractiveRebase" xml:space="preserve">Интерактивное перемещение (rebase -i) ${0}$ сюда</x:String>
<x:String x:Key="Text.CommitCM.Merge" xml:space="preserve">Влить в ${0}$</x:String>
@ -136,6 +146,7 @@
<x:String x:Key="Text.CommitDetail.Info.SHA" xml:space="preserve">SHA</x:String>
<x:String x:Key="Text.CommitDetail.Info.WebLinks" xml:space="preserve">Открыть в браузере</x:String>
<x:String x:Key="Text.CommitMessageTextBox.MessagePlaceholder" xml:space="preserve">Описание</x:String>
<x:String x:Key="Text.CommitMessageTextBox.SubjectCount" xml:space="preserve">СУБЪЕКТ</x:String>
<x:String x:Key="Text.CommitMessageTextBox.SubjectPlaceholder" xml:space="preserve">Введите тему ревизии</x:String>
<x:String x:Key="Text.Configure" xml:space="preserve">Настройка репозитория</x:String>
<x:String x:Key="Text.Configure.CommitMessageTemplate" xml:space="preserve">ШАБЛОН РЕВИЗИИ</x:String>
@ -199,7 +210,7 @@
<x:String x:Key="Text.CopyPath" xml:space="preserve">Копировать путь</x:String>
<x:String x:Key="Text.CreateBranch" xml:space="preserve">Создать ветку...</x:String>
<x:String x:Key="Text.CreateBranch.BasedOn" xml:space="preserve">Основан на:</x:String>
<x:String x:Key="Text.CreateBranch.Checkout" xml:space="preserve">Проверить созданную ветку</x:String>
<x:String x:Key="Text.CreateBranch.Checkout" xml:space="preserve">Переключиться на созданную ветку</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges" xml:space="preserve">Локальные изменения:</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.Discard" xml:space="preserve">Отклонить</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.StashAndReply" xml:space="preserve">Отложить и применить повторно</x:String>
@ -348,9 +359,9 @@
<x:String x:Key="Text.GitLFS.Locks.UnlockForce" xml:space="preserve">Принудительно разблокировать</x:String>
<x:String x:Key="Text.GitLFS.Prune" xml:space="preserve">Обрезать</x:String>
<x:String x:Key="Text.GitLFS.Prune.Tips" xml:space="preserve">Запустить (git lfs prune), чтобы удалить старые файлы LFS из локального хранилища</x:String>
<x:String x:Key="Text.GitLFS.Pull" xml:space="preserve">Забрать</x:String>
<x:String x:Key="Text.GitLFS.Pull" xml:space="preserve">Загрузить</x:String>
<x:String x:Key="Text.GitLFS.Pull.Tips" xml:space="preserve">Запустить (git lfs pull), чтобы загрузить все файлы LFS Git для текущей ссылки и проверить</x:String>
<x:String x:Key="Text.GitLFS.Pull.Title" xml:space="preserve">Забрать объекты LFS</x:String>
<x:String x:Key="Text.GitLFS.Pull.Title" xml:space="preserve">Загрузить объекты LFS</x:String>
<x:String x:Key="Text.GitLFS.Push" xml:space="preserve">Выложить</x:String>
<x:String x:Key="Text.GitLFS.Push.Tips" xml:space="preserve">Отправляйте большие файлы, помещенные в очередь, в конечную точку LFS Git</x:String>
<x:String x:Key="Text.GitLFS.Push.Title" xml:space="preserve">Выложить объекты LFS</x:String>
@ -385,7 +396,7 @@
<x:String x:Key="Text.Hotkeys.Repo.Fetch" xml:space="preserve">Извлечение, запускается сразу</x:String>
<x:String x:Key="Text.Hotkeys.Repo.GoHome" xml:space="preserve">Режим доски (по умолчанию)</x:String>
<x:String x:Key="Text.Hotkeys.Repo.OpenSearchCommits" xml:space="preserve">Режим поиска ревизий</x:String>
<x:String x:Key="Text.Hotkeys.Repo.Pull" xml:space="preserve">Забрать, запускается сразу</x:String>
<x:String x:Key="Text.Hotkeys.Repo.Pull" xml:space="preserve">Загрузить, запускается сразу</x:String>
<x:String x:Key="Text.Hotkeys.Repo.Push" xml:space="preserve">Выложить, запускается сразу</x:String>
<x:String x:Key="Text.Hotkeys.Repo.Refresh" xml:space="preserve">Принудительно перезагрузить репозиторий</x:String>
<x:String x:Key="Text.Hotkeys.Repo.StageOrUnstageSelected" xml:space="preserve">Подготовленные/Неподготовленные выбранные изменения</x:String>
@ -490,6 +501,7 @@
<x:String x:Key="Text.Preferences.Git.Email" xml:space="preserve">Электроная почта пользователя</x:String>
<x:String x:Key="Text.Preferences.Git.Email.Placeholder" xml:space="preserve">Общая электроная почта пользователя git</x:String>
<x:String x:Key="Text.Preferences.Git.EnablePruneOnFetch" xml:space="preserve">Разрешить (--prune) при скачивании</x:String>
<x:String x:Key="Text.Preferences.Git.IgnoreCRAtEOLInDiff" xml:space="preserve">Разрешить (--ignore-cr-at-eol) в различии</x:String>
<x:String x:Key="Text.Preferences.Git.Invalid" xml:space="preserve">Для работы программы требуется версия Git (&gt;= 2.23.0)</x:String>
<x:String x:Key="Text.Preferences.Git.Path" xml:space="preserve">Путь установки</x:String>
<x:String x:Key="Text.Preferences.Git.SSLVerify" xml:space="preserve">Разрешить верификацию HTTP SSL</x:String>
@ -512,16 +524,16 @@
<x:String x:Key="Text.PruneRemote.Target" xml:space="preserve">Цель:</x:String>
<x:String x:Key="Text.PruneWorktrees" xml:space="preserve">Удалить рабочий каталог</x:String>
<x:String x:Key="Text.PruneWorktrees.Tip" xml:space="preserve">Информация об обрезке рабочего каталога в «$GIT_COMMON_DIR/worktrees»</x:String>
<x:String x:Key="Text.Pull" xml:space="preserve">Забрать</x:String>
<x:String x:Key="Text.Pull" xml:space="preserve">Загрузить</x:String>
<x:String x:Key="Text.Pull.Branch" xml:space="preserve">Ветка внешнего репозитория:</x:String>
<x:String x:Key="Text.Pull.FetchAllBranches" xml:space="preserve">Извлечь все ветки</x:String>
<x:String x:Key="Text.Pull.Into" xml:space="preserve">В:</x:String>
<x:String x:Key="Text.Pull.LocalChanges" xml:space="preserve">Локальные изменения:</x:String>
<x:String x:Key="Text.Pull.LocalChanges.Discard" xml:space="preserve">Отклонить</x:String>
<x:String x:Key="Text.Pull.LocalChanges.StashAndReply" xml:space="preserve">Отложить и применить повторно</x:String>
<x:String x:Key="Text.Pull.NoTags" xml:space="preserve">Забрать без меток</x:String>
<x:String x:Key="Text.Pull.NoTags" xml:space="preserve">Загрузить без меток</x:String>
<x:String x:Key="Text.Pull.Remote" xml:space="preserve">Внешний репозиторий:</x:String>
<x:String x:Key="Text.Pull.Title" xml:space="preserve">Забрать (Получить и слить)</x:String>
<x:String x:Key="Text.Pull.Title" xml:space="preserve">Загрузить (Получить и слить)</x:String>
<x:String x:Key="Text.Pull.UseRebase" xml:space="preserve">Использовать перемещение вместо слияния</x:String>
<x:String x:Key="Text.Push" xml:space="preserve">Выложить</x:String>
<x:String x:Key="Text.Push.CheckSubmodules" xml:space="preserve">Убедитесь, что подмодули были вставлены</x:String>
@ -615,6 +627,7 @@
<x:String x:Key="Text.Repository.Tags.Sort" xml:space="preserve">Сортировать</x:String>
<x:String x:Key="Text.Repository.Terminal" xml:space="preserve">Открыть в терминале</x:String>
<x:String x:Key="Text.Repository.UseRelativeTimeInHistories" xml:space="preserve">Использовать относительное время в историях</x:String>
<x:String x:Key="Text.Repository.ViewLogs" xml:space="preserve">Просмотр логов</x:String>
<x:String x:Key="Text.Repository.Worktrees" xml:space="preserve">РАБОЧИЕ КАТАЛОГИ</x:String>
<x:String x:Key="Text.Repository.Worktrees.Add" xml:space="preserve">ДОБАВИТЬ РАБОЧИЙ КАТАЛОГ</x:String>
<x:String x:Key="Text.Repository.Worktrees.Prune" xml:space="preserve">ОБРЕЗАТЬ</x:String>
@ -700,6 +713,10 @@
<x:String x:Key="Text.UpdateSubmodules.Target" xml:space="preserve">Подмодуль:</x:String>
<x:String x:Key="Text.UpdateSubmodules.UseRemote" xml:space="preserve">Использовать опцию (--remote)</x:String>
<x:String x:Key="Text.URL" xml:space="preserve">Сетевой адрес:</x:String>
<x:String x:Key="Text.ViewLogs" xml:space="preserve">Логи</x:String>
<x:String x:Key="Text.ViewLogs.Clear" xml:space="preserve">ОЧИСТИТЬ ВСЁ</x:String>
<x:String x:Key="Text.ViewLogs.CopyLog" xml:space="preserve">Копировать</x:String>
<x:String x:Key="Text.ViewLogs.Delete" xml:space="preserve">Удалить</x:String>
<x:String x:Key="Text.Warn" xml:space="preserve">Предупреждение</x:String>
<x:String x:Key="Text.Welcome" xml:space="preserve">Приветствие</x:String>
<x:String x:Key="Text.Welcome.AddRootFolder" xml:space="preserve">Создать группу</x:String>

View file

@ -40,6 +40,13 @@
<x:String x:Key="Text.AssumeUnchanged.Empty" xml:space="preserve">没有不跟踪更改的文件</x:String>
<x:String x:Key="Text.AssumeUnchanged.Remove" xml:space="preserve">移除</x:String>
<x:String x:Key="Text.BinaryNotSupported" xml:space="preserve">二进制文件不支持该操作!!!</x:String>
<x:String x:Key="Text.Bisect">二分定位(bisect)</x:String>
<x:String x:Key="Text.Bisect.Abort">终止</x:String>
<x:String x:Key="Text.Bisect.Bad">标记错误</x:String>
<x:String x:Key="Text.Bisect.Detecting">二分定位进行中。当前提交是 '正确' 还是 '错误' </x:String>
<x:String x:Key="Text.Bisect.Good">标记正确</x:String>
<x:String x:Key="Text.Bisect.Skip">无法判定</x:String>
<x:String x:Key="Text.Bisect.WaitingForRange">二分定位进行中。请标记当前的提交是 '正确' 还是 '错误',然后检出另一个提交。</x:String>
<x:String x:Key="Text.Blame" xml:space="preserve">逐行追溯(blame)</x:String>
<x:String x:Key="Text.BlameTypeNotSupported" xml:space="preserve">选中文件不支持该操作!!!</x:String>
<x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">检出(checkout) ${0}$...</x:String>
@ -494,6 +501,7 @@
<x:String x:Key="Text.Preferences.Git.Email" xml:space="preserve">邮箱</x:String>
<x:String x:Key="Text.Preferences.Git.Email.Placeholder" xml:space="preserve">默认GIT用户邮箱</x:String>
<x:String x:Key="Text.Preferences.Git.EnablePruneOnFetch" xml:space="preserve">拉取更新时启用修剪(--prune</x:String>
<x:String x:Key="Text.Preferences.Git.IgnoreCRAtEOLInDiff" xml:space="preserve">对比文件时,默认忽略换行符变更 (--ignore-cr-at-eol)</x:String>
<x:String x:Key="Text.Preferences.Git.Invalid" xml:space="preserve">本软件要求GIT最低版本为2.23.0</x:String>
<x:String x:Key="Text.Preferences.Git.Path" xml:space="preserve">安装路径</x:String>
<x:String x:Key="Text.Preferences.Git.SSLVerify" xml:space="preserve">启用HTTP SSL验证</x:String>

View file

@ -40,6 +40,13 @@
<x:String x:Key="Text.AssumeUnchanged.Empty" xml:space="preserve">沒有不追蹤變更的檔案</x:String>
<x:String x:Key="Text.AssumeUnchanged.Remove" xml:space="preserve">移除</x:String>
<x:String x:Key="Text.BinaryNotSupported" xml:space="preserve">二進位檔案不支援該操作!</x:String>
<x:String x:Key="Text.Bisect">二分搜尋 (bisect)</x:String>
<x:String x:Key="Text.Bisect.Abort">中止</x:String>
<x:String x:Key="Text.Bisect.Bad">標記為錯誤</x:String>
<x:String x:Key="Text.Bisect.Detecting">二分搜尋進行中。目前的提交是「良好」是「錯誤」?</x:String>
<x:String x:Key="Text.Bisect.Good">標記為良好</x:String>
<x:String x:Key="Text.Bisect.Skip">無法確認</x:String>
<x:String x:Key="Text.Bisect.WaitingForRange">二分搜尋進行中。請標記目前的提交為「良好」或「錯誤」,然後簽出另一個提交。</x:String>
<x:String x:Key="Text.Blame" xml:space="preserve">逐行溯源 (blame)</x:String>
<x:String x:Key="Text.BlameTypeNotSupported" xml:space="preserve">所選擇的檔案不支援該操作!</x:String>
<x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">簽出 (checkout) ${0}$...</x:String>
@ -62,7 +69,7 @@
<x:String x:Key="Text.BranchCM.Rename" xml:space="preserve">重新命名 ${0}$...</x:String>
<x:String x:Key="Text.BranchCM.Tracking" xml:space="preserve">切換上游分支...</x:String>
<x:String x:Key="Text.BranchCompare" xml:space="preserve">分支比較</x:String>
<x:String x:Key="Text.BranchUpstreamInvalid" xml:space="preserve">追蹤上游分支不存在或已刪除</x:String>
<x:String x:Key="Text.BranchUpstreamInvalid" xml:space="preserve">追蹤上游分支不存在或已刪除!</x:String>
<x:String x:Key="Text.Bytes" xml:space="preserve">位元組</x:String>
<x:String x:Key="Text.Cancel" xml:space="preserve">取 消</x:String>
<x:String x:Key="Text.ChangeCM.CheckoutFirstParentRevision" xml:space="preserve">重設檔案到上一版本</x:String>
@ -161,7 +168,7 @@
<x:String x:Key="Text.Configure.Git.AutoFetch" xml:space="preserve">啟用定時自動提取 (fetch) 遠端更新</x:String>
<x:String x:Key="Text.Configure.Git.AutoFetchIntervalSuffix" xml:space="preserve">分鐘</x:String>
<x:String x:Key="Text.Configure.Git.DefaultRemote" xml:space="preserve">預設遠端存放庫</x:String>
<x:String x:Key="Text.Configure.Git.PreferredMergeMode" xml:space="preserve">首選合併模式</x:String>
<x:String x:Key="Text.Configure.Git.PreferredMergeMode" xml:space="preserve">預設合併模式</x:String>
<x:String x:Key="Text.Configure.IssueTracker" xml:space="preserve">Issue 追蹤</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleAzure" xml:space="preserve">新增符合 Azure DevOps 規則</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGiteeIssue" xml:space="preserve">新增符合 Gitee 議題規則</x:String>
@ -186,10 +193,10 @@
<x:String x:Key="Text.ConfigureWorkspace.Color" xml:space="preserve">顏色</x:String>
<x:String x:Key="Text.ConfigureWorkspace.Name" xml:space="preserve">名稱</x:String>
<x:String x:Key="Text.ConfigureWorkspace.Restore" xml:space="preserve">啟動時還原上次開啟的存放庫</x:String>
<x:String x:Key="Text.ConfirmEmptyCommit.Continue" xml:space="preserve">确认继续</x:String>
<x:String x:Key="Text.ConfirmEmptyCommit.Continue" xml:space="preserve">確認繼續</x:String>
<x:String x:Key="Text.ConfirmEmptyCommit.NoLocalChanges" xml:space="preserve">未包含任何檔案變更! 您是否仍要提交 (--allow-empty)?</x:String>
<x:String x:Key="Text.ConfirmEmptyCommit.StageAllThenCommit" xml:space="preserve">自动暂存并提交</x:String>
<x:String x:Key="Text.ConfirmEmptyCommit.WithLocalChanges" xml:space="preserve">未包含任何檔案變更! 您是否仍要提交 (--allow-empty)或者自動暫存全部變更並提交?</x:String>
<x:String x:Key="Text.ConfirmEmptyCommit.StageAllThenCommit" xml:space="preserve">自動暫存並提交</x:String>
<x:String x:Key="Text.ConfirmEmptyCommit.WithLocalChanges" xml:space="preserve">未包含任何檔案變更! 您是否仍要提交 (--allow-empty) 或者自動暫存全部變更並提交?</x:String>
<x:String x:Key="Text.ConventionalCommit" xml:space="preserve">產生約定式提交訊息</x:String>
<x:String x:Key="Text.ConventionalCommit.BreakingChanges" xml:space="preserve">破壞性變更:</x:String>
<x:String x:Key="Text.ConventionalCommit.ClosedIssue" xml:space="preserve">關閉的 Issue:</x:String>
@ -250,7 +257,7 @@
<x:String x:Key="Text.Diff.Copy" xml:space="preserve">複製</x:String>
<x:String x:Key="Text.Diff.FileModeChanged" xml:space="preserve">檔案權限已變更</x:String>
<x:String x:Key="Text.Diff.First" xml:space="preserve">第一個差異</x:String>
<x:String x:Key="Text.Diff.IgnoreWhitespace" xml:space="preserve">忽略空白符號變化和EOL</x:String>
<x:String x:Key="Text.Diff.IgnoreWhitespace" xml:space="preserve">忽略空白符號變化和 EOL</x:String>
<x:String x:Key="Text.Diff.Last" xml:space="preserve">最後一個差異</x:String>
<x:String x:Key="Text.Diff.LFS" xml:space="preserve">LFS 物件變更</x:String>
<x:String x:Key="Text.Diff.Next" xml:space="preserve">下一個差異</x:String>
@ -306,8 +313,8 @@
<x:String x:Key="Text.FileCM.Unstage" xml:space="preserve">取消暫存</x:String>
<x:String x:Key="Text.FileCM.UnstageMulti" xml:space="preserve">從暫存中移除 {0} 個檔案</x:String>
<x:String x:Key="Text.FileCM.UnstageSelectedLines" xml:space="preserve">取消暫存選取的變更</x:String>
<x:String x:Key="Text.FileCM.UseMine" xml:space="preserve">使用我方版本 (checkout --ours)</x:String>
<x:String x:Key="Text.FileCM.UseTheirs" xml:space="preserve">使用對方版本 (checkout --theirs)</x:String>
<x:String x:Key="Text.FileCM.UseMine" xml:space="preserve">使用我方版本 (ours)</x:String>
<x:String x:Key="Text.FileCM.UseTheirs" xml:space="preserve">使用對方版本 (theirs)</x:String>
<x:String x:Key="Text.FileHistory" xml:space="preserve">檔案歷史</x:String>
<x:String x:Key="Text.FileHistory.FileChange" xml:space="preserve">檔案變更</x:String>
<x:String x:Key="Text.FileHistory.FileContent" xml:space="preserve">檔案内容</x:String>
@ -465,7 +472,7 @@
<x:String x:Key="Text.Preferences.AI.Streaming" xml:space="preserve">啟用串流輸出</x:String>
<x:String x:Key="Text.Preferences.Appearance" xml:space="preserve">外觀設定</x:String>
<x:String x:Key="Text.Preferences.Appearance.DefaultFont" xml:space="preserve">預設字型</x:String>
<x:String x:Key="Text.Preferences.Appearance.EditorTabWidth" xml:space="preserve">編輯器制表符寬度</x:String>
<x:String x:Key="Text.Preferences.Appearance.EditorTabWidth" xml:space="preserve">編輯器 Tab 寬度</x:String>
<x:String x:Key="Text.Preferences.Appearance.FontSize" xml:space="preserve">字型大小</x:String>
<x:String x:Key="Text.Preferences.Appearance.FontSize.Default" xml:space="preserve">預設</x:String>
<x:String x:Key="Text.Preferences.Appearance.FontSize.Editor" xml:space="preserve">程式碼</x:String>
@ -494,6 +501,7 @@
<x:String x:Key="Text.Preferences.Git.Email" xml:space="preserve">電子郵件</x:String>
<x:String x:Key="Text.Preferences.Git.Email.Placeholder" xml:space="preserve">預設 Git 使用者電子郵件</x:String>
<x:String x:Key="Text.Preferences.Git.EnablePruneOnFetch" xml:space="preserve">拉取變更時進行清理 (--prune)</x:String>
<x:String x:Key="Text.Preferences.Git.IgnoreCRAtEOLInDiff" xml:space="preserve">對比檔案時,預設忽略行尾的 CR 變更 (--ignore-cr-at-eol)</x:String>
<x:String x:Key="Text.Preferences.Git.Invalid" xml:space="preserve">本軟體要求 Git 最低版本為 2.23.0</x:String>
<x:String x:Key="Text.Preferences.Git.Path" xml:space="preserve">安裝路徑</x:String>
<x:String x:Key="Text.Preferences.Git.SSLVerify" xml:space="preserve">啟用 HTTP SSL 驗證</x:String>
@ -619,7 +627,7 @@
<x:String x:Key="Text.Repository.Tags.Sort" xml:space="preserve">排序</x:String>
<x:String x:Key="Text.Repository.Terminal" xml:space="preserve">在終端機中開啟</x:String>
<x:String x:Key="Text.Repository.UseRelativeTimeInHistories" xml:space="preserve">在提交列表中使用相對時間</x:String>
<x:String x:Key="Text.Repository.ViewLogs" xml:space="preserve">檢視 GIT 指令的日誌</x:String>
<x:String x:Key="Text.Repository.ViewLogs" xml:space="preserve">檢視 Git 指令記錄</x:String>
<x:String x:Key="Text.Repository.Worktrees" xml:space="preserve">工作區列表</x:String>
<x:String x:Key="Text.Repository.Worktrees.Add" xml:space="preserve">新增工作區</x:String>
<x:String x:Key="Text.Repository.Worktrees.Prune" xml:space="preserve">清理</x:String>
@ -705,8 +713,8 @@
<x:String x:Key="Text.UpdateSubmodules.Target" xml:space="preserve">子模組:</x:String>
<x:String x:Key="Text.UpdateSubmodules.UseRemote" xml:space="preserve">啟用 [--remote] 選項</x:String>
<x:String x:Key="Text.URL" xml:space="preserve">存放庫網址:</x:String>
<x:String x:Key="Text.ViewLogs" xml:space="preserve">日誌清單</x:String>
<x:String x:Key="Text.ViewLogs.Clear" xml:space="preserve">清除所有日誌</x:String>
<x:String x:Key="Text.ViewLogs" xml:space="preserve">記錄</x:String>
<x:String x:Key="Text.ViewLogs.Clear" xml:space="preserve">清除所有記錄</x:String>
<x:String x:Key="Text.ViewLogs.CopyLog" xml:space="preserve">複製</x:String>
<x:String x:Key="Text.ViewLogs.Delete" xml:space="preserve">刪除</x:String>
<x:String x:Key="Text.Warn" xml:space="preserve">警告</x:String>
@ -738,13 +746,13 @@
<x:String x:Key="Text.WorkingCopy.CommitTip" xml:space="preserve">觸發點擊事件</x:String>
<x:String x:Key="Text.WorkingCopy.CommitToEdit" xml:space="preserve">提交 (修改原始提交)</x:String>
<x:String x:Key="Text.WorkingCopy.CommitWithAutoStage" xml:space="preserve">自動暫存全部變更並提交</x:String>
<x:String x:Key="Text.WorkingCopy.ConfirmCommitWithFilter" xml:space="preserve">您已暫存 {0} 檔案,但只顯示 {1} 檔案 ({2} 檔案被篩選器隱藏)。您要繼續嗎?</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts" xml:space="preserve">測到衝突</x:String>
<x:String x:Key="Text.WorkingCopy.ConfirmCommitWithFilter" xml:space="preserve">您已暫存 {0} 個檔案,但只顯示 {1} 個檔案 ({2} 個檔案被篩選器隱藏)。您確定要繼續提交嗎?</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts" xml:space="preserve">測到衝突</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.OpenExternalMergeTool" xml:space="preserve">使用外部合併工具開啟</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts" xml:space="preserve">使用外部合併工具開啟</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.Resolved" xml:space="preserve">檔案衝突已解決</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.UseMine" xml:space="preserve">使用 MINE</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.UseTheirs" xml:space="preserve">使用 THEIRS</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.UseMine" xml:space="preserve">使用我方版本 (ours)</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.UseTheirs" xml:space="preserve">使用對方版本 (theirs)</x:String>
<x:String x:Key="Text.WorkingCopy.IncludeUntracked" xml:space="preserve">顯示未追蹤檔案</x:String>
<x:String x:Key="Text.WorkingCopy.NoCommitHistories" xml:space="preserve">沒有提交訊息記錄</x:String>
<x:String x:Key="Text.WorkingCopy.NoCommitTemplates" xml:space="preserve">沒有可套用的提交訊息範本</x:String>

View file

@ -25,6 +25,7 @@
<Color x:Key="Color.Diff.AddedHighlight">#A7E1A7</Color>
<Color x:Key="Color.Diff.DeletedHighlight">#F19B9D</Color>
<Color x:Key="Color.Link">#0000EE</Color>
<Color x:Key="Color.InlineCode">#FFE5E5E5</Color>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
@ -51,6 +52,7 @@
<Color x:Key="Color.Diff.AddedHighlight">#A0308D3C</Color>
<Color x:Key="Color.Diff.DeletedHighlight">#A09F4247</Color>
<Color x:Key="Color.Link">#4DAAFC</Color>
<Color x:Key="Color.InlineCode">#FF2E2E2E</Color>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
@ -79,6 +81,7 @@
<SolidColorBrush x:Key="Brush.Diff.AddedHighlight" Color="{DynamicResource Color.Diff.AddedHighlight}"/>
<SolidColorBrush x:Key="Brush.Diff.DeletedHighlight" Color="{DynamicResource Color.Diff.DeletedHighlight}"/>
<SolidColorBrush x:Key="Brush.Link" Color="{DynamicResource Color.Link}"/>
<SolidColorBrush x:Key="Brush.InlineCode" Color="{DynamicResource Color.InlineCode}"/>
<FontFamily x:Key="Fonts.Default">fonts:Inter#Inter</FontFamily>
<FontFamily x:Key="Fonts.Monospace">fonts:SourceGit#JetBrains Mono</FontFamily>

View file

@ -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<Models.Change> changes, Action<string> 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<Models.Change> _changes = null;
private Action<string> _onApply = null;
private CancellationTokenSource _cancel = null;
private bool _isGenerating = false;
private string _text = string.Empty;
}
}

View file

@ -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<Models.Hyperlink> ParseLinksInMessage(string message)
private List<Models.InlineElement> ParseInlinesInMessage(string message)
{
var links = new List<Models.Hyperlink>();
var inlines = new List<Models.InlineElement>();
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()

View file

@ -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<Models.Commit> _commits = null;
private Dictionary<string, string> _fullCommitMessages = new Dictionary<string, string>();
private object _viewContent = null;
}
}

View file

@ -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));
@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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}...");

View file

@ -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<Models.Change> 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<Models.Change>();
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,8 +1738,19 @@ namespace SourceGit.ViewModels
UseAmend = false;
if (autoPush && _repo.Remotes.Count > 0)
{
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();
_repo.SetWatcherEnabled(true);

View file

@ -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 @@
<!-- Options -->
<Border Grid.Row="2" Margin="0,0,0,8">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<v:LoadingIcon x:Name="IconInProgress" Width="14" Height="14" Margin="0,0,8,0"/>
<v:LoadingIcon Width="14" Height="14"
Margin="0,0,8,0"
IsVisible="{Binding IsGenerating}"/>
<Button Classes="flat"
x:Name="BtnGenerateCommitMessage"
Height="28"
Margin="0,0,8,0"
Padding="12,0"
Content="{DynamicResource Text.AIAssistant.Use}"
Click="OnGenerateCommitMessage"/>
IsEnabled="{Binding !IsGenerating}"
Click="OnApply"/>
<Button Classes="flat"
x:Name="BtnRegenerate"
Height="28"
Padding="12,0"
Content="{DynamicResource Text.AIAssistant.Regen}"
Click="OnRegen"/>
IsEnabled="{Binding !IsGenerating}"
Command="{Binding Regen}"/>
</StackPanel>
</Border>
</Grid>

View file

@ -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<Models.Change> 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<Models.Change> _changes;
private CancellationTokenSource _cancel;
}
}

View file

@ -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<IBrush> BackgroundProperty =
AvaloniaProperty.Register<BisectStateIndicator, IBrush>(nameof(Background), Brushes.Transparent);
public IBrush Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
public static readonly StyledProperty<IBrush> ForegroundProperty =
AvaloniaProperty.Register<BisectStateIndicator, IBrush>(nameof(Foreground), Brushes.White);
public IBrush Foreground
{
get => GetValue(ForegroundProperty);
set => SetValue(ForegroundProperty, value);
}
public static readonly StyledProperty<Models.Bisect> BisectProperty =
AvaloniaProperty.Register<BisectStateIndicator, Models.Bisect>(nameof(Bisect));
public Models.Bisect Bisect
{
get => GetValue(BisectProperty);
set => SetValue(BisectProperty, value);
}
static BisectStateIndicator()
{
AffectsMeasure<BisectStateIndicator>(BisectProperty);
AffectsRender<BisectStateIndicator>(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;
}
}

View file

@ -32,6 +32,7 @@
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:BranchTreeNode">
<Border Background="Transparent" PointerPressed="OnNodePointerPressed">
<Grid Height="24"
Margin="{Binding Depth, Converter={x:Static c:IntConverters.ToTreeMargin}}"
ColumnDefinitions="16,*"
@ -87,6 +88,7 @@
Mode="{Binding FilterMode}"/>
</Grid>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

View file

@ -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)
{

View file

@ -18,6 +18,8 @@ namespace SourceGit.Views
public ChromelessWindow()
{
Focusable = true;
if (OperatingSystem.IsLinux())
{
if (UseSystemWindowFrame)

View file

@ -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<CommitBaseInfo>();
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<string, Models.Commit> _inlineCommits = new();
}
}

View file

@ -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;
}

View file

@ -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<FontFamily> FontFamilyProperty =
AvaloniaProperty.Register<CommitSubjectPresenter, FontFamily>(nameof(FontFamily));
public FontFamily FontFamily
{
get => GetValue(FontFamilyProperty);
set => SetValue(FontFamilyProperty, value);
}
public static readonly StyledProperty<FontFamily> CodeFontFamilyProperty =
AvaloniaProperty.Register<CommitSubjectPresenter, FontFamily>(nameof(CodeFontFamily));
public FontFamily CodeFontFamily
{
get => GetValue(CodeFontFamilyProperty);
set => SetValue(CodeFontFamilyProperty, value);
}
public static readonly StyledProperty<double> FontSizeProperty =
TextBlock.FontSizeProperty.AddOwner<CommitSubjectPresenter>();
public double FontSize
{
get => GetValue(FontSizeProperty);
set => SetValue(FontSizeProperty, value);
}
public static readonly StyledProperty<FontWeight> FontWeightProperty =
TextBlock.FontWeightProperty.AddOwner<CommitSubjectPresenter>();
public FontWeight FontWeight
{
get => GetValue(FontWeightProperty);
set => SetValue(FontWeightProperty, value);
}
public static readonly StyledProperty<IBrush> InlineCodeBackgroundProperty =
AvaloniaProperty.Register<CommitSubjectPresenter, IBrush>(nameof(InlineCodeBackground), Brushes.Transparent);
public IBrush InlineCodeBackground
{
get => GetValue(InlineCodeBackgroundProperty);
set => SetValue(InlineCodeBackgroundProperty, value);
}
public static readonly StyledProperty<IBrush> ForegroundProperty =
AvaloniaProperty.Register<CommitSubjectPresenter, IBrush>(nameof(Foreground), Brushes.White);
public IBrush Foreground
{
get => GetValue(ForegroundProperty);
set => SetValue(ForegroundProperty, value);
}
public static readonly StyledProperty<IBrush> LinkForegroundProperty =
AvaloniaProperty.Register<CommitSubjectPresenter, IBrush>(nameof(LinkForeground), Brushes.White);
public IBrush LinkForeground
{
get => GetValue(LinkForegroundProperty);
set => SetValue(LinkForegroundProperty, value);
}
public static readonly StyledProperty<string> SubjectProperty =
AvaloniaProperty.Register<CommitSubjectPresenter, string>(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);
var rules = IssueTrackerRules ?? [];
var matches = new List<Models.Hyperlink>();
foreach (var rule in rules)
rule.Matches(matches, 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;
}
_elements.Add(new Models.InlineElement(Models.InlineElementType.Keyword, 0, keywordMatch.Length, string.Empty));
matches.Sort((l, r) => l.Start - r.Start);
_matches = matches;
var codeMatches = REG_INLINECODE_FORMAT().Matches(subject);
for (var i = 0; i < codeMatches.Count; i++)
{
var match = codeMatches[i];
if (!match.Success)
continue;
var inlines = new List<Inline>();
var pos = 0;
foreach (var match in matches)
var start = match.Index;
var len = match.Length;
var intersect = false;
foreach (var exist in _elements)
{
if (match.Start > pos)
if (exist.Intersect(start, len))
{
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)));
intersect = true;
break;
}
}
var link = new Run(subject.Substring(match.Start, match.Length));
link.Classes.Add("issue_link");
inlines.Add(link);
if (intersect)
continue;
pos = match.Start + match.Length;
_elements.Add(new Models.InlineElement(Models.InlineElementType.Code, start, len, string.Empty));
}
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)));
}
}
var rules = IssueTrackerRules ?? [];
foreach (var rule in rules)
rule.Matches(_elements, subject);
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,32 +201,24 @@ 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);
var textPosition = TextLayout.HitTestPoint(point).TextPosition;
foreach (var match in _matches)
{
if (!match.Intersect(textPosition, 1))
if (inline.Element is not { Type: Models.InlineElementType.Link } link)
continue;
if (match == _lastHover)
return;
if (inline.X > point.X || inline.X + inline.Text.WidthIncludingTrailingWhitespace < point.X)
continue;
_lastHover = match;
_lastHover = link;
SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
ToolTip.SetTip(this, match.Link);
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<Models.Hyperlink> _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<Models.InlineElement> _elements = [];
private List<Inline> _inlines = [];
private Models.InlineElement _lastHover = null;
private bool _needRebuildInlines = false;
}
}

View file

@ -93,7 +93,9 @@
<TextBlock Grid.Column="3" Classes="primary" Text="{Binding AuthorTimeShortStr}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Right"/>
</Grid>
<TextBlock Grid.Row="1" Classes="primary" Text="{Binding Subject}" VerticalAlignment="Bottom"/>
<Border Grid.Row="1" Background="Transparent" DataContextChanged="OnCommitSubjectDataContextChanged" PointerMoved="OnCommitSubjectPointerMoved">
<TextBlock Classes="primary" Text="{Binding Subject}" VerticalAlignment="Bottom"/>
</Border>
</Grid>
</Border>
</DataTemplate>

View file

@ -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));
}
}
}
}

View file

@ -126,19 +126,21 @@
</Grid.ColumnDefinitions>
<!-- Subject & REFS -->
<Border Grid.Column="0"
Padding="{Binding Margin}"
ClipToBounds="True"
Background="Transparent"
ToolTip.Tip="{Binding Subject}">
<Grid ColumnDefinitions="Auto,Auto,*" Margin="2,0,4,0" ClipToBounds="True">
<Border Grid.Column="0" Padding="{Binding Margin}" ClipToBounds="True" Background="Transparent">
<Grid ColumnDefinitions="Auto,Auto,Auto,*" Margin="2,0,4,0" ClipToBounds="True">
<v:CommitStatusIndicator Grid.Column="0"
CurrentBranch="{Binding $parent[v:Histories].CurrentBranch}"
AheadBrush="{DynamicResource Brush.Accent}"
BehindBrush="{DynamicResource Brush.FG1}"
VerticalAlignment="Center"/>
<v:CommitRefsPresenter Grid.Column="1"
<v:BisectStateIndicator Grid.Column="1"
Background="{DynamicResource Brush.Contents}"
Foreground="{DynamicResource Brush.FG1}"
Bisect="{Binding $parent[v:Histories].Bisect}"
VerticalAlignment="Center"/>
<v:CommitRefsPresenter Grid.Column="2"
Background="{DynamicResource Brush.Contents}"
Foreground="{DynamicResource Brush.FG1}"
FontFamily="{DynamicResource Fonts.Primary}"
@ -153,8 +155,12 @@
</v:CommitRefsPresenter.UseGraphColor>
</v:CommitRefsPresenter>
<v:CommitSubjectPresenter Grid.Column="2"
Classes="primary"
<v:CommitSubjectPresenter Grid.Column="3"
FontFamily="{DynamicResource Fonts.Primary}"
CodeFontFamily="{DynamicResource Fonts.Monospace}"
InlineCodeBackground="{DynamicResource Brush.InlineCode}"
Foreground="{DynamicResource Brush.FG1}"
LinkForeground="{DynamicResource Brush.Link}"
Subject="{Binding Subject}"
IssueTrackerRules="{Binding $parent[v:Histories].IssueTrackerRules}"
FontWeight="{Binding FontWeight}"
@ -163,10 +169,7 @@
</Border>
<!-- Author -->
<Grid Grid.Column="1"
ColumnDefinitions="20,*"
Background="Transparent"
ToolTip.Tip="{Binding Author}">
<Grid Grid.Column="1" ColumnDefinitions="20,*" Background="Transparent">
<v:Avatar Grid.Column="0"
Width="16" Height="16"
Margin="4,0,0,0"

View file

@ -91,6 +91,15 @@ namespace SourceGit.Views
set => SetValue(CurrentBranchProperty, value);
}
public static readonly StyledProperty<Models.Bisect> BisectProperty =
AvaloniaProperty.Register<Histories, Models.Bisect>(nameof(Bisect));
public Models.Bisect Bisect
{
get => GetValue(BisectProperty);
set => SetValue(BisectProperty, value);
}
public static readonly StyledProperty<AvaloniaList<Models.IssueTrackerRule>> IssueTrackerRulesProperty =
AvaloniaProperty.Register<Histories, AvaloniaList<Models.IssueTrackerRule>>(nameof(IssueTrackerRules));

View file

@ -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();
}
}
}

View file

@ -45,7 +45,7 @@
<Path Width="14" Height="14" Data="{StaticResource Icons.Explore}"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}">
<MenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}" InputGesture="F1">
<MenuItem.Icon>
<Path Width="14" Height="14" Data="{StaticResource Icons.Hotkeys}"/>
</MenuItem.Icon>

View file

@ -136,11 +136,18 @@ namespace SourceGit.Views
// Ctrl+Shift+P opens preference dialog (macOS use hotkeys in system menu bar)
if (!OperatingSystem.IsMacOS() && e is { KeyModifiers: (KeyModifiers.Control | KeyModifiers.Shift), Key: Key.P })
{
App.OpenDialog(new Preferences());
App.ShowWindow(new Preferences(), true);
e.Handled = true;
return;
}
// F1 opens preference dialog (macOS use hotkeys in system menu bar)
if (!OperatingSystem.IsMacOS() && e.Key == Key.F1)
{
App.ShowWindow(new Hotkeys(), true);
return;
}
// Ctrl+Q quits the application (macOS use hotkeys in system menu bar)
if (!OperatingSystem.IsMacOS() && e.KeyModifiers == KeyModifiers.Control && e.Key == Key.Q)
{

View file

@ -68,7 +68,7 @@ namespace SourceGit.Views
return;
}
var viewTypeName = dataTypeName.Replace("ViewModels", "Views");
var viewTypeName = dataTypeName.Replace(".ViewModels.", ".Views.");
var viewType = Type.GetType(viewTypeName);
if (viewType == null)
{

View file

@ -15,8 +15,7 @@
<TextBlock Margin="6,0,0,0" FontSize="14" FontWeight="Bold" Text="{DynamicResource Text.Running}"/>
</StackPanel>
<TextBlock x:Name="TxtDesc"
HorizontalAlignment="Stretch"
<TextBlock HorizontalAlignment="Stretch"
TextWrapping="Wrap"
FontSize="{Binding Source={x:Static vm:Preferences.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Decrease}}"
FontStyle="Italic"

View file

@ -21,17 +21,9 @@ namespace SourceGit.Views
InitializeComponent();
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
if (IsVisible)
StartAnim();
}
protected override void OnUnloaded(RoutedEventArgs e)
{
StopAnim();
_isUnloading = true;
base.OnUnloaded(e);
}
@ -41,7 +33,7 @@ namespace SourceGit.Views
if (change.Property == IsVisibleProperty)
{
if (IsVisible)
if (IsVisible && !_isUnloading)
StartAnim();
else
StopAnim();
@ -61,5 +53,7 @@ namespace SourceGit.Views
Icon.Content = null;
ProgressBar.IsIndeterminate = false;
}
private bool _isUnloading = false;
}
}

View file

@ -273,7 +273,7 @@
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preferences.Git}"/>
</TabItem.Header>
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32,32" ColumnDefinitions="Auto,*">
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32,32,32" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preferences.Git.Path}"
HorizontalAlignment="Right"
@ -352,6 +352,11 @@
IsChecked="{Binding #ThisControl.EnablePruneOnFetch, Mode=TwoWay}"/>
<CheckBox Grid.Row="6" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preferences.Git.IgnoreCRAtEOLInDiff}"
IsChecked="{Binding IgnoreCRAtEOLInDiff, Mode=TwoWay}"/>
<CheckBox Grid.Row="7" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preferences.Git.SSLVerify}"
IsChecked="{Binding #ThisControl.EnableHTTPSSLVerify, Mode=TwoWay}"/>

View file

@ -349,9 +349,7 @@ namespace SourceGit.Views
if (sender is CheckBox box)
{
ViewModels.Preferences.Instance.UseSystemWindowFrame = box.IsChecked == true;
var dialog = new ConfirmRestart();
App.OpenDialog(dialog);
App.ShowWindow(new ConfirmRestart(), true);
}
e.Handled = true;

View file

@ -585,7 +585,7 @@
BorderBrush="{DynamicResource Brush.Border0}"/>
<!-- Right -->
<Grid Grid.Column="2" RowDefinitions="Auto,Auto,*">
<Grid Grid.Column="2" RowDefinitions="Auto,Auto,Auto,*">
<Grid Grid.Row="0" Height="28" ColumnDefinitions="*,Auto" Background="{DynamicResource Brush.Conflict}" IsVisible="{Binding InProgressContext, Converter={x:Static ObjectConverters.IsNotNull}}">
<ContentControl Grid.Column="0" Margin="8,0" Content="{Binding InProgressContext}">
<ContentControl.DataTemplates>
@ -665,7 +665,73 @@
Command="{Binding AbortMerge}"/>
</Grid>
<Border Grid.Row="1" Background="{DynamicResource Brush.ToolBar}" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}">
<Grid Grid.Row="1"
Height="28"
ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto"
Background="{DynamicResource Brush.Conflict}"
IsVisible="{Binding BisectState, Converter={x:Static ObjectConverters.NotEqual}, ConverterParameter={x:Static m:BisectState.None}}">
<TextBlock Grid.Column="0"
FontWeight="Bold"
Margin="8,0"
Foreground="{DynamicResource Brush.ConflictForeground}"
Text="{DynamicResource Text.Bisect.WaitingForRange}"
IsVisible="{Binding BisectState, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:BisectState.WaitingForRange}}"/>
<TextBlock Grid.Column="0"
FontWeight="Bold"
Margin="8,0"
Foreground="{DynamicResource Brush.ConflictForeground}"
Text="{DynamicResource Text.Bisect.Detecting}"
IsVisible="{Binding BisectState, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:BisectState.Detecting}}"/>
<v:LoadingIcon Grid.Column="1"
Width="14" Height="14"
Margin="4,0"
IsVisible="{Binding IsBisectCommandRunning}"/>
<Button Grid.Column="2"
Classes="flat"
Margin="4,0"
FontWeight="Regular"
BorderThickness="0"
Content="{DynamicResource Text.Bisect.Good}"
IsEnabled="{Binding !IsBisectCommandRunning}"
Padding="8,2"
Click="OnBisectCommand"
Tag="good"/>
<Button Grid.Column="3"
Classes="flat"
Margin="4,0"
FontWeight="Regular"
BorderThickness="0"
Content="{DynamicResource Text.Bisect.Bad}"
IsEnabled="{Binding !IsBisectCommandRunning}"
Padding="8,2"
Click="OnBisectCommand"
Tag="bad"/>
<Button Grid.Column="4"
Classes="flat"
Margin="4,0"
FontWeight="Regular"
BorderThickness="0"
Content="{DynamicResource Text.Bisect.Skip}"
IsEnabled="{Binding !IsBisectCommandRunning}"
Padding="8,2"
Click="OnBisectCommand"
Tag="skip"/>
<Button Grid.Column="5"
Classes="flat"
Margin="4,0"
FontWeight="Regular"
BorderThickness="0"
Content="{DynamicResource Text.Bisect.Abort}"
IsEnabled="{Binding !IsBisectCommandRunning}"
Padding="8,2"
Click="OnBisectCommand"
Tag="reset"/>
</Grid>
<Border Grid.Row="2" Background="{DynamicResource Brush.ToolBar}" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}">
<Border.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="SelectedViewIndex" Converter="{x:Static c:IntConverters.IsZero}"/>
@ -721,10 +787,11 @@
</Grid>
</Border>
<ContentControl Grid.Row="2" Content="{Binding SelectedView}">
<ContentControl Grid.Row="3" Content="{Binding SelectedView}">
<ContentControl.DataTemplates>
<DataTemplate DataType="vm:Histories">
<v:Histories CurrentBranch="{Binding Repo.CurrentBranch}"
Bisect="{Binding Bisect}"
AuthorNameColumnWidth="{Binding Source={x:Static vm:Preferences.Instance}, Path=Layout.HistoriesAuthorColumnWidth, Mode=TwoWay}"
IssueTrackerRules="{Binding Repo.Settings.IssueTrackerRules}"
OnlyHighlightCurrentBranch="{Binding Repo.OnlyHighlightCurrentBranchInHistories}"

View file

@ -429,5 +429,15 @@ namespace SourceGit.Views
e.Handled = true;
}
private void OnBisectCommand(object sender, RoutedEventArgs e)
{
if (sender is Button button &&
DataContext is ViewModels.Repository { IsBisectCommandRunning: false } repo &&
repo.CanCreatePopup())
repo.Bisect(button.Tag as string);
e.Handled = true;
}
}
}

View file

@ -100,6 +100,10 @@
<Path Width="14" Height="14" Data="{StaticResource Icons.LFS}"/>
</Button>
<Button Classes="icon_button" Width="32" Margin="8,0,0,0" Click="StartBisect" ToolTip.Tip="{DynamicResource Text.Bisect}">
<Path Width="14" Height="14" Data="{StaticResource Icons.Bisect}"/>
</Button>
<Button Classes="icon_button" Width="32" Margin="8,0,0,0" Click="OpenCustomActionMenu" ToolTip.Tip="{DynamicResource Text.Repository.CustomActions}">
<Path Width="14" Height="14" Data="{StaticResource Icons.Action}"/>
</Button>

View file

@ -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;
}
}

View file

@ -26,11 +26,10 @@
SelectionChanged="OnRowSelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:TagTreeNode">
<Border Height="24" Background="Transparent" PointerPressed="OnRowPointerPressed" DoubleTapped="OnDoubleTappedNode" ContextRequested="OnRowContextRequested">
<Grid ColumnDefinitions="16,Auto,*,Auto"
Margin="{Binding Depth, Converter={x:Static c:IntConverters.ToTreeMargin}}"
Background="Transparent"
ContextRequested="OnRowContextRequested"
DoubleTapped="OnDoubleTappedNode"
VerticalAlignment="Center"
ToolTip.Tip="{Binding ToolTip}">
<v:TagTreeNodeToggleButton Grid.Column="0"
Classes="tree_expander"
@ -56,6 +55,7 @@
</ContentControl.DataTemplates>
</ContentControl>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
@ -69,10 +69,8 @@
SelectionChanged="OnRowSelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Tag">
<Grid ColumnDefinitions="Auto,*,Auto"
Background="Transparent"
ContextRequested="OnRowContextRequested"
ToolTip.Tip="{Binding Message}">
<Border Height="24" Background="Transparent" PointerPressed="OnRowPointerPressed" ContextRequested="OnRowContextRequested">
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center" ToolTip.Tip="{Binding Message}">
<Path Grid.Column="0"
Margin="8,0,0,0"
Width="12" Height="12"
@ -86,6 +84,7 @@
<v:FilterModeSwitchButton Grid.Column="2" Margin="0,0,12,0" Mode="{Binding FilterMode}"/>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

View file

@ -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)
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<ViewModels.TagTreeNode> rows, List<ViewModels.TagTreeNode> nodes)

View file

@ -96,6 +96,13 @@
</ListBox.ItemTemplate>
</ListBox>
<Path Grid.Column="0"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="48" Height="48"
Data="{StaticResource Icons.Empty}"
Fill="{DynamicResource Brush.FG2}"
IsVisible="{Binding Logs.Count, Converter={x:Static c:IntConverters.IsZero}}"/>
<Border Grid.Column="2"
BorderBrush="{DynamicResource Brush.Border2}"
BorderThickness="1"