Merge branch 'release/v2025.09'

This commit is contained in:
leo 2025-03-17 09:37:49 +08:00
commit 6fd6bbb6b5
No known key found for this signature in database
74 changed files with 1582 additions and 1473 deletions

View file

@ -47,7 +47,7 @@
## Translation Status
[![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)](TRANSLATION.md) [![de__DE](https://img.shields.io/badge/de__DE-99.08%25-yellow)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-%E2%88%9A-brightgreen)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-91.68%25-yellow)](TRANSLATION.md) [![it__IT](https://img.shields.io/badge/it__IT-99.87%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-91.41%25-yellow)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen)](TRANSLATION.md) [![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen)](TRANSLATION.md) [![zh__TW](https://img.shields.io/badge/zh__TW-%E2%88%9A-brightgreen)](TRANSLATION.md)
[![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)](TRANSLATION.md) [![de__DE](https://img.shields.io/badge/de__DE-99.07%25-yellow)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-%E2%88%9A-brightgreen)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-91.66%25-yellow)](TRANSLATION.md) [![it__IT](https://img.shields.io/badge/it__IT-99.87%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-91.39%25-yellow)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen)](TRANSLATION.md) [![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen)](TRANSLATION.md) [![zh__TW](https://img.shields.io/badge/zh__TW-%E2%88%9A-brightgreen)](TRANSLATION.md)
> [!NOTE]
> You can find the missing keys in [TRANSLATION.md](TRANSLATION.md)
@ -79,7 +79,7 @@ For **Windows** users:
```
> [!NOTE]
> `winget` will install this software as a commandline tool. You need run `SourceGit` from console or `Win+R` at the first time. Then you can add it to the taskbar.
* You can install the latest stable by `scoope` with follow commands:
* You can install the latest stable by `scoop` with follow commands:
```shell
scoop bucket add extras
scoop install sourcegit

View file

@ -1,4 +1,4 @@
### de_DE.axaml: 99.08%
### de_DE.axaml: 99.07%
<details>
@ -24,7 +24,7 @@
</details>
### fr_FR.axaml: 91.68%
### fr_FR.axaml: 91.66%
<details>
@ -106,7 +106,7 @@
</details>
### pt_BR.axaml: 91.41%
### pt_BR.axaml: 91.39%
<details>

View file

@ -1 +1 @@
2025.08
2025.09

View file

@ -10,7 +10,7 @@ cd build
rm -rf SourceGit/*.pdb
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
powershell -Command "Compress-Archive -Path SourceGit\\* -DestinationPath \"sourcegit_$VERSION.$RUNTIME.zip\" -Force"
powershell -Command "Compress-Archive -Path SourceGit -DestinationPath \"sourcegit_$VERSION.$RUNTIME.zip\" -Force"
else
zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit
zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit
fi

View file

@ -77,6 +77,31 @@ namespace SourceGit
Native.OS.SetupApp(builder);
return builder;
}
private static void LogException(Exception ex)
{
if (ex == null)
return;
var builder = new StringBuilder();
builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n");
builder.Append("----------------------------\n");
builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n");
builder.Append($"OS: {Environment.OSVersion}\n");
builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n");
builder.Append($"Source: {ex.Source}\n");
builder.Append($"Thread Name: {Thread.CurrentThread.Name ?? "Unnamed"}\n");
builder.Append($"User: {Environment.UserName}\n");
builder.Append($"App Start Time: {Process.GetCurrentProcess().StartTime}\n");
builder.Append($"Exception Time: {DateTime.Now}\n");
builder.Append($"Memory Usage: {Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024} MB\n");
builder.Append($"---------------------------\n\n");
builder.Append(ex);
var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log");
File.WriteAllText(file, builder.ToString());
}
#endregion
#region Utility Functions
@ -181,6 +206,9 @@ namespace SourceGit
app._fontsOverrides = null;
}
defaultFont = app.FixFontFamilyName(defaultFont);
monospaceFont = app.FixFontFamilyName(monospaceFont);
var resDic = new ResourceDictionary();
if (!string.IsNullOrEmpty(defaultFont))
resDic.Add("Fonts.Default", new FontFamily(defaultFont));
@ -325,31 +353,6 @@ namespace SourceGit
}
#endregion
private static void LogException(Exception ex)
{
if (ex == null)
return;
var builder = new StringBuilder();
builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n");
builder.Append("----------------------------\n");
builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n");
builder.Append($"OS: {Environment.OSVersion}\n");
builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n");
builder.Append($"Source: {ex.Source}\n");
builder.Append($"Thread Name: {Thread.CurrentThread.Name ?? "Unnamed"}\n");
builder.Append($"User: {Environment.UserName}\n");
builder.Append($"App Start Time: {Process.GetCurrentProcess().StartTime}\n");
builder.Append($"Exception Time: {DateTime.Now}\n");
builder.Append($"Memory Usage: {Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024} MB\n");
builder.Append($"---------------------------\n\n");
builder.Append(ex);
var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log");
File.WriteAllText(file, builder.ToString());
}
private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode)
{
exitCode = -1;
@ -546,6 +549,24 @@ namespace SourceGit
});
}
private string FixFontFamilyName(string input)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
var parts = input.Split(',');
var trimmed = new List<string>();
foreach (var part in parts)
{
var t = part.Trim();
if (!string.IsNullOrEmpty(t))
trimmed.Add(t);
}
return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty;
}
private ViewModels.Launcher _launcher = null;
private ResourceDictionary _activeLocale = null;
private ResourceDictionary _themeOverrides = null;

View file

@ -54,21 +54,14 @@
public static bool DeleteRemote(string repo, string remote, string name)
{
bool exists = new Remote(repo).HasBranch(remote, name);
if (exists)
return new Push(repo, remote, $"refs/heads/{name}", true).Exec();
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
bool exists = new Remote(repo).HasBranch(remote, name);
if (exists)
{
cmd.SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
cmd.Args = $"push {remote} --delete {name}";
}
else
{
cmd.Args = $"branch -D -r {remote}/{name}";
}
cmd.Args = $"branch -D -r {remote}/{name}";
return cmd.Exec();
}
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Avalonia.Threading;
@ -10,11 +11,6 @@ namespace SourceGit.Commands
{
public partial class Command
{
public class CancelToken
{
public bool Requested { get; set; } = false;
}
public class ReadToEndResult
{
public bool IsSuccess { get; set; } = false;
@ -30,7 +26,7 @@ namespace SourceGit.Commands
}
public string Context { get; set; } = string.Empty;
public CancelToken Cancel { get; set; } = null;
public CancellationToken CancellationToken { get; set; } = CancellationToken.None;
public string WorkingDirectory { get; set; } = null;
public EditorType Editor { get; set; } = EditorType.CoreEditor; // Only used in Exec() mode
public string SSHKey { get; set; } = string.Empty;
@ -43,36 +39,15 @@ namespace SourceGit.Commands
var start = CreateGitStartInfo();
var errs = new List<string>();
var proc = new Process() { StartInfo = start };
var isCancelled = false;
proc.OutputDataReceived += (_, e) =>
{
if (Cancel != null && Cancel.Requested)
{
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
if (!proc.HasExited)
proc.Kill(true);
return;
}
if (e.Data != null)
OnReadline(e.Data);
};
proc.ErrorDataReceived += (_, e) =>
{
if (Cancel != null && Cancel.Requested)
{
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
if (!proc.HasExited)
proc.Kill(true);
return;
}
if (string.IsNullOrEmpty(e.Data))
{
errs.Add(string.Empty);
@ -97,9 +72,25 @@ namespace SourceGit.Commands
errs.Add(e.Data);
};
var dummy = null as Process;
var dummyProcLock = new object();
try
{
proc.Start();
// It not safe, please only use `CancellationToken` in readonly commands.
if (CancellationToken.CanBeCanceled)
{
dummy = proc;
CancellationToken.Register(() =>
{
lock (dummyProcLock)
{
if (dummy is { HasExited: false })
dummy.Kill();
}
});
}
}
catch (Exception e)
{
@ -113,10 +104,18 @@ namespace SourceGit.Commands
proc.BeginErrorReadLine();
proc.WaitForExit();
if (dummy != null)
{
lock (dummyProcLock)
{
dummy = null;
}
}
int exitCode = proc.ExitCode;
proc.Close();
if (!isCancelled && exitCode != 0)
if (!CancellationToken.IsCancellationRequested && exitCode != 0)
{
if (RaiseError)
{
@ -192,13 +191,12 @@ namespace SourceGit.Commands
if (!start.Environment.ContainsKey("GIT_SSH_COMMAND") && !string.IsNullOrEmpty(SSHKey))
start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}'");
// Force using en_US.UTF-8 locale to avoid GCM crash
// Force using en_US.UTF-8 locale
if (OperatingSystem.IsLinux())
start.Environment.Add("LANG", "en_US.UTF-8");
// Fix macOS `PATH` env
if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv))
start.Environment.Add("PATH", Native.OS.CustomPathEnv);
{
start.Environment.Add("LANG", "C");
start.Environment.Add("LC_ALL", "C");
}
// Force using this app as git editor.
switch (Editor)

View file

@ -8,7 +8,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = "status -uno --ignore-submodules=dirty --porcelain";
Args = "--no-optional-locks status -uno --ignore-submodules=dirty --porcelain";
}
public int Result()

View file

@ -17,14 +17,6 @@ namespace SourceGit.Commands
start.CreateNoWindow = true;
start.WorkingDirectory = repo;
// Force using en_US.UTF-8 locale to avoid GCM crash
if (OperatingSystem.IsLinux())
start.Environment.Add("LANG", "en_US.UTF-8");
// Fix macOS `PATH` env
if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv))
start.Environment.Add("PATH", Native.OS.CustomPathEnv);
try
{
Process.Start(start);
@ -48,14 +40,6 @@ namespace SourceGit.Commands
start.StandardErrorEncoding = Encoding.UTF8;
start.WorkingDirectory = repo;
// Force using en_US.UTF-8 locale to avoid GCM crash
if (OperatingSystem.IsLinux())
start.Environment.Add("LANG", "en_US.UTF-8");
// Fix macOS `PATH` env
if (OperatingSystem.IsMacOS() && !string.IsNullOrEmpty(Native.OS.CustomPathEnv))
start.Environment.Add("PATH", Native.OS.CustomPathEnv);
var proc = new Process() { StartInfo = start };
var builder = new StringBuilder();

View file

@ -26,7 +26,7 @@ namespace SourceGit.Commands
Args += $"{remote} {local}:{remoteBranch}";
}
public Push(string repo, string remote, string tag, bool isDelete)
public Push(string repo, string remote, string refname, bool isDelete)
{
WorkingDirectory = repo;
Context = repo;
@ -36,7 +36,7 @@ namespace SourceGit.Commands
if (isDelete)
Args += "--delete ";
Args += $"{remote} refs/tags/{tag}";
Args += $"{remote} {refname}";
}
protected override void OnReadline(string line)

View file

@ -9,7 +9,7 @@ namespace SourceGit.Commands
WorkingDirectory = repo;
Context = repo;
_commit = commit;
Args = $"rev-list -{max} --parents --branches --remotes ^{commit}";
Args = $"rev-list -{max} --parents --branches --remotes --ancestry-path={commit} ^{commit}";
}
public List<string> Result()

View file

@ -13,7 +13,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = $"status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain";
Args = $"--no-optional-locks status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain";
}
public List<Models.Change> Result()

View file

@ -1,4 +1,6 @@
namespace SourceGit.Commands
using System.Collections.Generic;
namespace SourceGit.Commands
{
public class QueryRevisionFileNames : Command
{
@ -9,13 +11,17 @@
Args = $"ls-tree -r -z --name-only {revision}";
}
public string[] Result()
public List<string> Result()
{
var rs = ReadToEnd();
if (rs.IsSuccess)
return rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries);
if (!rs.IsSuccess)
return [];
return [];
var lines = rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries);
var outs = new List<string>();
foreach (var line in lines)
outs.Add(line);
return outs;
}
}
}

View file

@ -49,7 +49,7 @@ namespace SourceGit.Commands
if (submodules.Count > 0)
{
Args = $"status -uno --porcelain -- {builder}";
Args = $"--no-optional-locks status -uno --porcelain -- {builder}";
rs = ReadToEnd();
if (!rs.IsSuccess)
return submodules;

View file

@ -48,9 +48,7 @@ namespace SourceGit.Commands
if (remotes != null)
{
foreach (var r in remotes)
{
new Push(repo, r.Name, name, true).Exec();
}
new Push(repo, r.Name, $"refs/tags/{name}", true).Exec();
}
return true;

View file

@ -8,6 +8,7 @@ namespace SourceGit.Models
{
public enum CommitSearchMethod
{
BySHA = 0,
ByAuthor,
ByCommitter,
ByMessage,
@ -35,6 +36,7 @@ namespace SourceGit.Models
public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly);
public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly);
public bool IsMerged { get; set; } = false;
public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime;

View file

@ -1,9 +0,0 @@
namespace SourceGit.Models
{
public enum DealWithLocalChanges
{
DoNothing,
StashAndReaply,
Discard,
}
}

View file

@ -471,11 +471,7 @@ namespace SourceGit.Models
public CustomAction AddNewCustomAction()
{
var act = new CustomAction()
{
Name = "Unnamed Custom Action",
};
var act = new CustomAction() { Name = "Unnamed Action" };
CustomActions.Add(act);
return act;
}

View file

@ -43,6 +43,11 @@ namespace SourceGit.Models
return _caches.GetOrAdd(data, key => new User(key));
}
public override string ToString()
{
return $"{Name} <{Email}>";
}
private static ConcurrentDictionary<string, User> _caches = new ConcurrentDictionary<string, User>();
private readonly int _hash;
}

View file

@ -18,9 +18,22 @@ namespace SourceGit.Native
DisableDefaultApplicationMenuItems = true,
});
// Fix `PATH` env on macOS.
var path = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrEmpty(path))
path = "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
else if (!path.Contains("/opt/homebrew/", StringComparison.Ordinal))
path = "/opt/homebrew/bin:/opt/homebrew/sbin:" + path;
var customPathFile = Path.Combine(OS.DataDir, "PATH");
if (File.Exists(customPathFile))
OS.CustomPathEnv = File.ReadAllText(customPathFile).Trim();
{
var env = File.ReadAllText(customPathFile).Trim();
if (!string.IsNullOrEmpty(env))
path = env;
}
Environment.SetEnvironmentVariable("PATH", path);
}
public string FindGitExecutable()

View file

@ -31,12 +31,6 @@ namespace SourceGit.Native
private set;
} = string.Empty;
public static string CustomPathEnv
{
get;
set;
} = string.Empty;
public static string GitExecutable
{
get => _gitExecutable;

View file

@ -8,6 +8,7 @@ using System.Text;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Threading;
namespace SourceGit.Native
{
@ -214,12 +215,17 @@ namespace SourceGit.Native
private void FixWindowFrameOnWin10(Window w)
{
var platformHandle = w.TryGetPlatformHandle();
if (platformHandle == null)
return;
// Schedule the DWM frame extension to run in the next render frame
// to ensure proper timing with the window initialization sequence
Dispatcher.UIThread.InvokeAsync(() =>
{
var platformHandle = w.TryGetPlatformHandle();
if (platformHandle == null)
return;
var margins = new MARGINS { cxLeftWidth = 1, cxRightWidth = 1, cyTopHeight = 1, cyBottomHeight = 1 };
DwmExtendFrameIntoClientArea(platformHandle.Handle, ref margins);
var margins = new MARGINS { cxLeftWidth = 1, cxRightWidth = 1, cyTopHeight = 1, cyBottomHeight = 1 };
DwmExtendFrameIntoClientArea(platformHandle.Handle, ref margins);
}, DispatcherPriority.Render);
}
#region EXTERNAL_EDITOR_FINDER

View file

@ -90,7 +90,6 @@
<x:String x:Key="Text.Checkout.Target" xml:space="preserve">Branch:</x:String>
<x:String x:Key="Text.Checkout.LocalChanges" xml:space="preserve">Lokale Änderungen:</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.Discard" xml:space="preserve">Verwerfen</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.DoNothing" xml:space="preserve">Nichts tun</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.StashAndReply" xml:space="preserve">Stashen &amp; wieder anwenden</x:String>
<x:String x:Key="Text.CherryPick" xml:space="preserve">Cherry Pick</x:String>
<x:String x:Key="Text.CherryPick.AppendSourceToMessage" xml:space="preserve">Quelle an Commit-Nachricht anhängen</x:String>
@ -206,7 +205,6 @@
<x:String x:Key="Text.CreateBranch.Checkout" xml:space="preserve">Erstellten Branch auschecken</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges" xml:space="preserve">Lokale Änderungen:</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.Discard" xml:space="preserve">Verwerfen</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.DoNothing" xml:space="preserve">Nichts tun</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.StashAndReply" xml:space="preserve">Stashen &amp; wieder anwenden</x:String>
<x:String x:Key="Text.CreateBranch.Name" xml:space="preserve">Neuer Branch-Name:</x:String>
<x:String x:Key="Text.CreateBranch.Name.Placeholder" xml:space="preserve">Branch-Namen eingeben.</x:String>
@ -445,6 +443,7 @@
<x:String x:Key="Text.Paste" xml:space="preserve">Einfügen</x:String>
<x:String x:Key="Text.Period.JustNow" xml:space="preserve">Gerade eben</x:String>
<x:String x:Key="Text.Period.MinutesAgo" xml:space="preserve">Vor {0} Minuten</x:String>
<x:String x:Key="Text.Period.HourAgo" xml:space="preserve">Vor 1 Stunde</x:String>
<x:String x:Key="Text.Period.HoursAgo" xml:space="preserve">Vor {0} Stunden</x:String>
<x:String x:Key="Text.Period.Yesterday" xml:space="preserve">Gestern</x:String>
<x:String x:Key="Text.Period.DaysAgo" xml:space="preserve">Vor {0} Tagen</x:String>
@ -517,7 +516,6 @@
<x:String x:Key="Text.Pull.Into" xml:space="preserve">Lokaler Branch:</x:String>
<x:String x:Key="Text.Pull.LocalChanges" xml:space="preserve">Lokale Änderungen:</x:String>
<x:String x:Key="Text.Pull.LocalChanges.Discard" xml:space="preserve">Verwerfen</x:String>
<x:String x:Key="Text.Pull.LocalChanges.DoNothing" xml:space="preserve">Nichts tun</x:String>
<x:String x:Key="Text.Pull.LocalChanges.StashAndReply" xml:space="preserve">Stashen &amp; wieder anwenden</x:String>
<x:String x:Key="Text.Pull.NoTags" xml:space="preserve">Ohne Tags fetchen</x:String>
<x:String x:Key="Text.Pull.Remote" xml:space="preserve">Remote:</x:String>

View file

@ -88,7 +88,6 @@
<x:String x:Key="Text.Checkout.Target" xml:space="preserve">Branch:</x:String>
<x:String x:Key="Text.Checkout.LocalChanges" xml:space="preserve">Local Changes:</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.Discard" xml:space="preserve">Discard</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.DoNothing" xml:space="preserve">Do Nothing</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.StashAndReply" xml:space="preserve">Stash &amp; Reapply</x:String>
<x:String x:Key="Text.CherryPick" xml:space="preserve">Cherry Pick</x:String>
<x:String x:Key="Text.CherryPick.AppendSourceToMessage" xml:space="preserve">Append source to commit message</x:String>
@ -205,7 +204,6 @@
<x:String x:Key="Text.CreateBranch.Checkout" xml:space="preserve">Check out the created branch</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges" xml:space="preserve">Local Changes:</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.Discard" xml:space="preserve">Discard</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.DoNothing" xml:space="preserve">Do Nothing</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.StashAndReply" xml:space="preserve">Stash &amp; Reapply</x:String>
<x:String x:Key="Text.CreateBranch.Name" xml:space="preserve">New Branch Name:</x:String>
<x:String x:Key="Text.CreateBranch.Name.Placeholder" xml:space="preserve">Enter branch name.</x:String>
@ -446,6 +444,7 @@
<x:String x:Key="Text.Paste" xml:space="preserve">Paste</x:String>
<x:String x:Key="Text.Period.JustNow" xml:space="preserve">Just now</x:String>
<x:String x:Key="Text.Period.MinutesAgo" xml:space="preserve">{0} minutes ago</x:String>
<x:String x:Key="Text.Period.HourAgo" xml:space="preserve">1 hour ago</x:String>
<x:String x:Key="Text.Period.HoursAgo" xml:space="preserve">{0} hours ago</x:String>
<x:String x:Key="Text.Period.Yesterday" xml:space="preserve">Yesterday</x:String>
<x:String x:Key="Text.Period.DaysAgo" xml:space="preserve">{0} days ago</x:String>
@ -520,7 +519,6 @@
<x:String x:Key="Text.Pull.Into" xml:space="preserve">Into:</x:String>
<x:String x:Key="Text.Pull.LocalChanges" xml:space="preserve">Local Changes:</x:String>
<x:String x:Key="Text.Pull.LocalChanges.Discard" xml:space="preserve">Discard</x:String>
<x:String x:Key="Text.Pull.LocalChanges.DoNothing" xml:space="preserve">Do Nothing</x:String>
<x:String x:Key="Text.Pull.LocalChanges.StashAndReply" xml:space="preserve">Stash &amp; Reapply</x:String>
<x:String x:Key="Text.Pull.NoTags" xml:space="preserve">Fetch without tags</x:String>
<x:String x:Key="Text.Pull.Remote" xml:space="preserve">Remote:</x:String>

View file

@ -91,7 +91,6 @@
<x:String x:Key="Text.Checkout.Target" xml:space="preserve">Rama:</x:String>
<x:String x:Key="Text.Checkout.LocalChanges" xml:space="preserve">Cambios Locales:</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.Discard" xml:space="preserve">Descartar</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.DoNothing" xml:space="preserve">No Hacer Nada</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.StashAndReply" xml:space="preserve">Stash &amp; Reaplicar</x:String>
<x:String x:Key="Text.CherryPick" xml:space="preserve">Cherry Pick</x:String>
<x:String x:Key="Text.CherryPick.AppendSourceToMessage" xml:space="preserve">Añadir fuente al mensaje de commit</x:String>
@ -208,7 +207,6 @@
<x:String x:Key="Text.CreateBranch.Checkout" xml:space="preserve">Checkout de la rama creada</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges" xml:space="preserve">Cambios Locales:</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.Discard" xml:space="preserve">Descartar</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.DoNothing" xml:space="preserve">No Hacer Nada</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.StashAndReply" xml:space="preserve">Stash &amp; Reaplicar</x:String>
<x:String x:Key="Text.CreateBranch.Name" xml:space="preserve">Nombre de la Nueva Rama:</x:String>
<x:String x:Key="Text.CreateBranch.Name.Placeholder" xml:space="preserve">Introduzca el nombre de la rama.</x:String>
@ -449,6 +447,7 @@
<x:String x:Key="Text.Paste" xml:space="preserve">Pegar</x:String>
<x:String x:Key="Text.Period.JustNow" xml:space="preserve">Justo ahora</x:String>
<x:String x:Key="Text.Period.MinutesAgo" xml:space="preserve">Hace {0} minutos</x:String>
<x:String x:Key="Text.Period.HourAgo" xml:space="preserve">Hace 1 hora</x:String>
<x:String x:Key="Text.Period.HoursAgo" xml:space="preserve">Hace {0} horas</x:String>
<x:String x:Key="Text.Period.Yesterday" xml:space="preserve">Ayer</x:String>
<x:String x:Key="Text.Period.DaysAgo" xml:space="preserve">Hace {0} días</x:String>
@ -524,7 +523,6 @@
<x:String x:Key="Text.Pull.Into" xml:space="preserve">En:</x:String>
<x:String x:Key="Text.Pull.LocalChanges" xml:space="preserve">Cambios Locales:</x:String>
<x:String x:Key="Text.Pull.LocalChanges.Discard" xml:space="preserve">Descartar</x:String>
<x:String x:Key="Text.Pull.LocalChanges.DoNothing" xml:space="preserve">No Hacer Nada</x:String>
<x:String x:Key="Text.Pull.LocalChanges.StashAndReply" xml:space="preserve">Stash &amp; Reaplicar</x:String>
<x:String x:Key="Text.Pull.NoTags" xml:space="preserve">Fetch sin etiquetas</x:String>
<x:String x:Key="Text.Pull.Remote" xml:space="preserve">Remoto:</x:String>

View file

@ -83,7 +83,6 @@
<x:String x:Key="Text.Checkout.Target" xml:space="preserve">Branche :</x:String>
<x:String x:Key="Text.Checkout.LocalChanges" xml:space="preserve">Changements locaux :</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.Discard" xml:space="preserve">Annuler</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.DoNothing" xml:space="preserve">Ne rien faire</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.StashAndReply" xml:space="preserve">Mettre en stash et réappliquer</x:String>
<x:String x:Key="Text.CherryPick" xml:space="preserve">Cherry-Pick de ce commit</x:String>
<x:String x:Key="Text.CherryPick.AppendSourceToMessage" xml:space="preserve">Ajouter la source au message de commit</x:String>
@ -198,7 +197,6 @@
<x:String x:Key="Text.CreateBranch.Checkout" xml:space="preserve">Récupérer la branche créée</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges" xml:space="preserve">Changements locaux :</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.Discard" xml:space="preserve">Rejeter</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.DoNothing" xml:space="preserve">Ne rien faire</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.StashAndReply" xml:space="preserve">Stash &amp; Réappliquer</x:String>
<x:String x:Key="Text.CreateBranch.Name" xml:space="preserve">Nom de la nouvelle branche :</x:String>
<x:String x:Key="Text.CreateBranch.Name.Placeholder" xml:space="preserve">Entrez le nom de la branche.</x:String>
@ -424,6 +422,7 @@
<x:String x:Key="Text.Paste" xml:space="preserve">Coller</x:String>
<x:String x:Key="Text.Period.JustNow" xml:space="preserve">A l'instant</x:String>
<x:String x:Key="Text.Period.MinutesAgo" xml:space="preserve">il y a {0} minutes</x:String>
<x:String x:Key="Text.Period.HourAgo" xml:space="preserve">il y a 1 heure</x:String>
<x:String x:Key="Text.Period.HoursAgo" xml:space="preserve">il y a {0} heures</x:String>
<x:String x:Key="Text.Period.Yesterday" xml:space="preserve">Hier</x:String>
<x:String x:Key="Text.Period.DaysAgo" xml:space="preserve">il y a {0} jours</x:String>
@ -492,7 +491,6 @@
<x:String x:Key="Text.Pull.Into" xml:space="preserve">Dans :</x:String>
<x:String x:Key="Text.Pull.LocalChanges" xml:space="preserve">Changements locaux :</x:String>
<x:String x:Key="Text.Pull.LocalChanges.Discard" xml:space="preserve">Rejeter</x:String>
<x:String x:Key="Text.Pull.LocalChanges.DoNothing" xml:space="preserve">Ne rien faire</x:String>
<x:String x:Key="Text.Pull.LocalChanges.StashAndReply" xml:space="preserve">Stash &amp; Réappliquer</x:String>
<x:String x:Key="Text.Pull.NoTags" xml:space="preserve">Fetch sans les tags</x:String>
<x:String x:Key="Text.Pull.Remote" xml:space="preserve">Dépôt distant :</x:String>

View file

@ -91,7 +91,6 @@
<x:String x:Key="Text.Checkout.Target" xml:space="preserve">Branch:</x:String>
<x:String x:Key="Text.Checkout.LocalChanges" xml:space="preserve">Modifiche Locali:</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.Discard" xml:space="preserve">Scarta</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.DoNothing" xml:space="preserve">Non fare nulla</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.StashAndReply" xml:space="preserve">Stasha e Ripristina</x:String>
<x:String x:Key="Text.CherryPick" xml:space="preserve">Cherry Pick</x:String>
<x:String x:Key="Text.CherryPick.AppendSourceToMessage" xml:space="preserve">Aggiungi sorgente al messaggio di commit</x:String>
@ -208,7 +207,6 @@
<x:String x:Key="Text.CreateBranch.Checkout" xml:space="preserve">Checkout del Branch Creato</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges" xml:space="preserve">Modifiche Locali:</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.Discard" xml:space="preserve">Scarta</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.DoNothing" xml:space="preserve">Non Fare Nulla</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.StashAndReply" xml:space="preserve">Stasha e Ripristina</x:String>
<x:String x:Key="Text.CreateBranch.Name" xml:space="preserve">Nome Nuovo Branch:</x:String>
<x:String x:Key="Text.CreateBranch.Name.Placeholder" xml:space="preserve">Inserisci il nome del branch.</x:String>
@ -450,6 +448,7 @@
<x:String x:Key="Text.Paste" xml:space="preserve">Incolla</x:String>
<x:String x:Key="Text.Period.JustNow" xml:space="preserve">Proprio ora</x:String>
<x:String x:Key="Text.Period.MinutesAgo" xml:space="preserve">{0} minuti fa</x:String>
<x:String x:Key="Text.Period.HourAgo" xml:space="preserve">1 ora fa</x:String>
<x:String x:Key="Text.Period.HoursAgo" xml:space="preserve">{0} ore fa</x:String>
<x:String x:Key="Text.Period.Yesterday" xml:space="preserve">Ieri</x:String>
<x:String x:Key="Text.Period.DaysAgo" xml:space="preserve">{0} giorni fa</x:String>
@ -523,7 +522,6 @@
<x:String x:Key="Text.Pull.Into" xml:space="preserve">In:</x:String>
<x:String x:Key="Text.Pull.LocalChanges" xml:space="preserve">Modifiche Locali:</x:String>
<x:String x:Key="Text.Pull.LocalChanges.Discard" xml:space="preserve">Scarta</x:String>
<x:String x:Key="Text.Pull.LocalChanges.DoNothing" xml:space="preserve">Non fare nulla</x:String>
<x:String x:Key="Text.Pull.LocalChanges.StashAndReply" xml:space="preserve">Stasha e Riapplica</x:String>
<x:String x:Key="Text.Pull.NoTags" xml:space="preserve">Recupera senza tag</x:String>
<x:String x:Key="Text.Pull.Remote" xml:space="preserve">Remoto:</x:String>

View file

@ -107,7 +107,6 @@
<x:String x:Key="Text.Checkout.Target" xml:space="preserve">Branch:</x:String>
<x:String x:Key="Text.Checkout.LocalChanges" xml:space="preserve">Alterações Locais:</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.Discard" xml:space="preserve">Descartar</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.DoNothing" xml:space="preserve">Nada</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.StashAndReply" xml:space="preserve">Stash &amp; Reaplicar</x:String>
<x:String x:Key="Text.CherryPick" xml:space="preserve">Cherry-Pick</x:String>
<x:String x:Key="Text.CherryPick.AppendSourceToMessage" xml:space="preserve">Adicionar origem à mensagem de commit</x:String>
@ -215,7 +214,6 @@
<x:String x:Key="Text.CreateBranch.Checkout" xml:space="preserve">Checar o branch criado</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges" xml:space="preserve">Alterações Locais:</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.Discard" xml:space="preserve">Descartar</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.DoNothing" xml:space="preserve">Não Fazer Nada</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.StashAndReply" xml:space="preserve">Guardar &amp; Reaplicar</x:String>
<x:String x:Key="Text.CreateBranch.Name" xml:space="preserve">Nome do Novo Branch:</x:String>
<x:String x:Key="Text.CreateBranch.Name.Placeholder" xml:space="preserve">Insira o nome do branch.</x:String>
@ -437,6 +435,7 @@
<x:String x:Key="Text.Paste" xml:space="preserve">Colar</x:String>
<x:String x:Key="Text.Period.JustNow" xml:space="preserve">Agora mesmo</x:String>
<x:String x:Key="Text.Period.MinutesAgo" xml:space="preserve">{0} minutos atrás</x:String>
<x:String x:Key="Text.Period.HourAgo" xml:space="preserve">1 hora atrás</x:String>
<x:String x:Key="Text.Period.HoursAgo" xml:space="preserve">{0} horas atrás</x:String>
<x:String x:Key="Text.Period.Yesterday" xml:space="preserve">Ontem</x:String>
<x:String x:Key="Text.Period.DaysAgo" xml:space="preserve">{0} dias atrás</x:String>
@ -506,7 +505,6 @@
<x:String x:Key="Text.Pull.Into" xml:space="preserve">Para:</x:String>
<x:String x:Key="Text.Pull.LocalChanges" xml:space="preserve">Alterações Locais:</x:String>
<x:String x:Key="Text.Pull.LocalChanges.Discard" xml:space="preserve">Descartar</x:String>
<x:String x:Key="Text.Pull.LocalChanges.DoNothing" xml:space="preserve">Não Fazer Nada</x:String>
<x:String x:Key="Text.Pull.LocalChanges.StashAndReply" xml:space="preserve">Guardar &amp; Reaplicar</x:String>
<x:String x:Key="Text.Pull.NoTags" xml:space="preserve">Buscar sem tags</x:String>
<x:String x:Key="Text.Pull.Remote" xml:space="preserve">Remoto:</x:String>

View file

@ -11,9 +11,9 @@
<x:String x:Key="Text.About.SourceCode" xml:space="preserve">• Исходный код можно найти по адресу </x:String>
<x:String x:Key="Text.About.SubTitle" xml:space="preserve">Бесплатный графический клиент Git с исходным кодом</x:String>
<x:String x:Key="Text.AddWorktree" xml:space="preserve">Добавить рабочий каталог</x:String>
<x:String x:Key="Text.AddWorktree.WhatToCheckout" 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" 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.Location" xml:space="preserve">Расположение:</x:String>
<x:String x:Key="Text.AddWorktree.Location.Placeholder" xml:space="preserve">Путь к рабочему каталогу (поддерживается относительный путь)</x:String>
<x:String x:Key="Text.AddWorktree.Name" xml:space="preserve">Имя ветки:</x:String>
@ -91,7 +91,6 @@
<x:String x:Key="Text.Checkout.Target" xml:space="preserve">Ветка:</x:String>
<x:String x:Key="Text.Checkout.LocalChanges" xml:space="preserve">Локальные изменения:</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.Discard" xml:space="preserve">Отклонить</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.DoNothing" xml:space="preserve">Ничего не делать</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.StashAndReply" xml:space="preserve">Отложить и примненить повторно</x:String>
<x:String x:Key="Text.CherryPick" xml:space="preserve"> Частичный выбор</x:String>
<x:String x:Key="Text.CherryPick.AppendSourceToMessage" xml:space="preserve">Добавить источник для ревизии сообщения</x:String>
@ -126,7 +125,7 @@
<x:String x:Key="Text.CommitCM.Reset" xml:space="preserve">Сбросить ${0}$ сюда</x:String>
<x:String x:Key="Text.CommitCM.Revert" xml:space="preserve">Отменить ревизию</x:String>
<x:String x:Key="Text.CommitCM.Reword" xml:space="preserve">Изменить комментарий</x:String>
<x:String x:Key="Text.CommitCM.SaveAsPatch" xml:space="preserve">Сохранить как patch-файл...</x:String>
<x:String x:Key="Text.CommitCM.SaveAsPatch" xml:space="preserve">Сохранить как заплатки...</x:String>
<x:String x:Key="Text.CommitCM.Squash" xml:space="preserve">Объединить с предыдущей ревизией</x:String>
<x:String x:Key="Text.CommitCM.SquashCommitsSinceThis" xml:space="preserve">Объединить все следующие ревизии с этим</x:String>
<x:String x:Key="Text.CommitDetail.Changes" xml:space="preserve">ИЗМЕНЕНИЯ</x:String>
@ -167,7 +166,7 @@
<x:String x:Key="Text.Configure.Email" xml:space="preserve">Адрес электронной почты</x:String>
<x:String x:Key="Text.Configure.Email.Placeholder" xml:space="preserve">Адрес электронной почты</x:String>
<x:String x:Key="Text.Configure.Git" xml:space="preserve">GIT</x:String>
<x:String x:Key="Text.Configure.Git.AutoFetch" xml:space="preserve">Автоматическая загрузка изменений</x:String>
<x:String x:Key="Text.Configure.Git.AutoFetch" xml:space="preserve">Автозагрузка изменений</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.IssueTracker" xml:space="preserve">ОТСЛЕЖИВАНИЕ ПРОБЛЕМ</x:String>
@ -209,7 +208,6 @@
<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.DoNothing" xml:space="preserve">Ничего не делать</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.StashAndReply" xml:space="preserve">Отложить и применить повторно</x:String>
<x:String x:Key="Text.CreateBranch.Name" xml:space="preserve">Имя новой ветки:</x:String>
<x:String x:Key="Text.CreateBranch.Name.Placeholder" xml:space="preserve">Введите имя ветки.</x:String>
@ -218,15 +216,15 @@
<x:String x:Key="Text.CreateTag" xml:space="preserve">Создать метку...</x:String>
<x:String x:Key="Text.CreateTag.BasedOn" xml:space="preserve">Новая метка у:</x:String>
<x:String x:Key="Text.CreateTag.GPGSign" xml:space="preserve">GPG подпись</x:String>
<x:String x:Key="Text.CreateTag.Message" xml:space="preserve">Сообщение с меткой:</x:String>
<x:String x:Key="Text.CreateTag.Message" xml:space="preserve">Сообщение с&#13;меткой:</x:String>
<x:String x:Key="Text.CreateTag.Message.Placeholder" xml:space="preserve">Необязательно.</x:String>
<x:String x:Key="Text.CreateTag.Name" xml:space="preserve">Имя метки:</x:String>
<x:String x:Key="Text.CreateTag.Name.Placeholder" xml:space="preserve">Рекомендуемый формат: v1.0.0-alpha</x:String>
<x:String x:Key="Text.CreateTag.PushToAllRemotes" xml:space="preserve">Выложить на все внешние репозитории после создания</x:String>
<x:String x:Key="Text.CreateTag.Title" xml:space="preserve">Создать новую метку</x:String>
<x:String x:Key="Text.CreateTag.Type" xml:space="preserve">Вид:</x:String>
<x:String x:Key="Text.CreateTag.Type.Annotated" xml:space="preserve">Аннотированный</x:String>
<x:String x:Key="Text.CreateTag.Type.Lightweight" xml:space="preserve">Лёгкий</x:String>
<x:String x:Key="Text.CreateTag.Type.Annotated" xml:space="preserve">С примечаниями</x:String>
<x:String x:Key="Text.CreateTag.Type.Lightweight" xml:space="preserve">Простой</x:String>
<x:String x:Key="Text.CtrlClickTip" xml:space="preserve">Удерживайте Ctrl, чтобы начать сразу</x:String>
<x:String x:Key="Text.Cut" xml:space="preserve">Вырезать</x:String>
<x:String x:Key="Text.DeleteBranch" xml:space="preserve">Удалить ветку</x:String>
@ -237,7 +235,7 @@
<x:String x:Key="Text.DeleteMultiBranch.Tip" xml:space="preserve">Вы пытаетесь удалить несколько веток одновременно. Обязательно перепроверьте, прежде чем предпринимать какие-либо действия!</x:String>
<x:String x:Key="Text.DeleteRemote" xml:space="preserve">Удалить внешний репозиторий</x:String>
<x:String x:Key="Text.DeleteRemote.Remote" xml:space="preserve">Внешний репозиторий:</x:String>
<x:String x:Key="Text.DeleteRepositoryNode.Path" xml:space="preserve">Path:</x:String>
<x:String x:Key="Text.DeleteRepositoryNode.Path" xml:space="preserve">Путь:</x:String>
<x:String x:Key="Text.DeleteRepositoryNode.Target" xml:space="preserve">Цель:</x:String>
<x:String x:Key="Text.DeleteRepositoryNode.TipForGroup" xml:space="preserve">Все дочерние элементы будут удалены из списка.</x:String>
<x:String x:Key="Text.DeleteRepositoryNode.TitleForGroup" xml:space="preserve">Подтвердите удаление группы</x:String>
@ -262,7 +260,7 @@
<x:String x:Key="Text.Diff.NoChange" xml:space="preserve">НИКАКИХ ИЗМЕНЕНИЙ ИЛИ МЕНЯЕТСЯ ТОЛЬКО EOL</x:String>
<x:String x:Key="Text.Diff.Prev" xml:space="preserve">Предыдущее различие</x:String>
<x:String x:Key="Text.Diff.SaveAsPatch" xml:space="preserve">Сохранить как заплатку</x:String>
<x:String x:Key="Text.Diff.SideBySide" xml:space="preserve">Различие бок о бок</x:String>
<x:String x:Key="Text.Diff.SideBySide" xml:space="preserve">Различие рядом</x:String>
<x:String x:Key="Text.Diff.Submodule" xml:space="preserve">ПОДМОДУЛЬ</x:String>
<x:String x:Key="Text.Diff.Submodule.New" xml:space="preserve">НОВЫЙ</x:String>
<x:String x:Key="Text.Diff.SwapCommits" xml:space="preserve">Обмен</x:String>
@ -450,6 +448,7 @@
<x:String x:Key="Text.Paste" xml:space="preserve">Вставить</x:String>
<x:String x:Key="Text.Period.JustNow" xml:space="preserve">Сейчас</x:String>
<x:String x:Key="Text.Period.MinutesAgo" xml:space="preserve">{0} минут назад</x:String>
<x:String x:Key="Text.Period.HourAgo" xml:space="preserve">1 час назад</x:String>
<x:String x:Key="Text.Period.HoursAgo" xml:space="preserve">{0} часов назад</x:String>
<x:String x:Key="Text.Period.Yesterday" xml:space="preserve">Вчера</x:String>
<x:String x:Key="Text.Period.DaysAgo" xml:space="preserve">{0} дней назад</x:String>
@ -524,7 +523,6 @@
<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.DoNothing" 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.Remote" xml:space="preserve">Внешний репозиторий:</x:String>
@ -553,7 +551,7 @@
<x:String x:Key="Text.Remote.EditTitle" xml:space="preserve">Редактировать внешний репозиторий</x:String>
<x:String x:Key="Text.Remote.Name" xml:space="preserve">Имя:</x:String>
<x:String x:Key="Text.Remote.Name.Placeholder" xml:space="preserve">Имя внешнего репозитория</x:String>
<x:String x:Key="Text.Remote.URL" xml:space="preserve">Адрес репозитория:</x:String>
<x:String x:Key="Text.Remote.URL" xml:space="preserve">Адрес:</x:String>
<x:String x:Key="Text.Remote.URL.Placeholder" xml:space="preserve">Адрес внешнего репозитория git</x:String>
<x:String x:Key="Text.RemoteCM.CopyURL" xml:space="preserve">Копировать адрес</x:String>
<x:String x:Key="Text.RemoteCM.Delete" xml:space="preserve">Удалить...</x:String>
@ -693,8 +691,8 @@
<x:String x:Key="Text.Submodule.CopyPath" xml:space="preserve">Копировать относительный путь</x:String>
<x:String x:Key="Text.Submodule.FetchNested" xml:space="preserve">Извлечение вложенных подмодулей</x:String>
<x:String x:Key="Text.Submodule.Open" xml:space="preserve">Открыть подмодуль репозитория</x:String>
<x:String x:Key="Text.Submodule.RelativePath" xml:space="preserve">Относительный путь:</x:String>
<x:String x:Key="Text.Submodule.RelativePath.Placeholder" xml:space="preserve">Относительный каталог для хранения подмодуля.</x:String>
<x:String x:Key="Text.Submodule.RelativePath" xml:space="preserve">Каталог:</x:String>
<x:String x:Key="Text.Submodule.RelativePath.Placeholder" xml:space="preserve">Относительный путь для хранения подмодуля.</x:String>
<x:String x:Key="Text.Submodule.Remove" xml:space="preserve">Удалить подмодуль</x:String>
<x:String x:Key="Text.Sure" xml:space="preserve">ОК</x:String>
<x:String x:Key="Text.TagCM.Copy" xml:space="preserve">Копировать имя метки</x:String>

View file

@ -91,7 +91,6 @@
<x:String x:Key="Text.Checkout.Target" xml:space="preserve">目标分支 </x:String>
<x:String x:Key="Text.Checkout.LocalChanges" xml:space="preserve">未提交更改 </x:String>
<x:String x:Key="Text.Checkout.LocalChanges.Discard" xml:space="preserve">丢弃更改</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.DoNothing" xml:space="preserve">不做处理</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.StashAndReply" xml:space="preserve">贮藏并自动恢复</x:String>
<x:String x:Key="Text.CherryPick" xml:space="preserve">挑选提交</x:String>
<x:String x:Key="Text.CherryPick.AppendSourceToMessage" xml:space="preserve">提交信息中追加来源信息</x:String>
@ -208,7 +207,6 @@
<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.DoNothing" xml:space="preserve">不做处理</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.StashAndReply" xml:space="preserve">贮藏并自动恢复</x:String>
<x:String x:Key="Text.CreateBranch.Name" xml:space="preserve">新分支名 </x:String>
<x:String x:Key="Text.CreateBranch.Name.Placeholder" xml:space="preserve">填写分支名称。</x:String>
@ -449,6 +447,7 @@
<x:String x:Key="Text.Paste" xml:space="preserve">粘贴</x:String>
<x:String x:Key="Text.Period.JustNow" xml:space="preserve">刚刚</x:String>
<x:String x:Key="Text.Period.MinutesAgo" xml:space="preserve">{0}分钟前</x:String>
<x:String x:Key="Text.Period.HourAgo" xml:space="preserve">1小时前</x:String>
<x:String x:Key="Text.Period.HoursAgo" xml:space="preserve">{0}小时前</x:String>
<x:String x:Key="Text.Period.Yesterday" xml:space="preserve">昨天</x:String>
<x:String x:Key="Text.Period.DaysAgo" xml:space="preserve">{0}天前</x:String>
@ -524,7 +523,6 @@
<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.DoNothing" 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.Remote" xml:space="preserve">远程 </x:String>

View file

@ -91,7 +91,6 @@
<x:String x:Key="Text.Checkout.Target" xml:space="preserve">目標分支:</x:String>
<x:String x:Key="Text.Checkout.LocalChanges" xml:space="preserve">未提交變更:</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.Discard" xml:space="preserve">捨棄變更</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.DoNothing" xml:space="preserve">不做處理</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.StashAndReply" xml:space="preserve">擱置變更並自動復原</x:String>
<x:String x:Key="Text.CherryPick" xml:space="preserve">揀選提交</x:String>
<x:String x:Key="Text.CherryPick.AppendSourceToMessage" xml:space="preserve">提交資訊中追加來源資訊</x:String>
@ -208,7 +207,6 @@
<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.DoNothing" xml:space="preserve">不做處理</x:String>
<x:String x:Key="Text.CreateBranch.LocalChanges.StashAndReply" xml:space="preserve">擱置變更並自動復原</x:String>
<x:String x:Key="Text.CreateBranch.Name" xml:space="preserve">新分支名稱:</x:String>
<x:String x:Key="Text.CreateBranch.Name.Placeholder" xml:space="preserve">輸入分支名稱。</x:String>
@ -449,6 +447,7 @@
<x:String x:Key="Text.Paste" xml:space="preserve">貼上</x:String>
<x:String x:Key="Text.Period.JustNow" xml:space="preserve">剛剛</x:String>
<x:String x:Key="Text.Period.MinutesAgo" xml:space="preserve">{0} 分鐘前</x:String>
<x:String x:Key="Text.Period.HourAgo" xml:space="preserve">1 小時前</x:String>
<x:String x:Key="Text.Period.HoursAgo" xml:space="preserve">{0} 小時前</x:String>
<x:String x:Key="Text.Period.Yesterday" xml:space="preserve">昨天</x:String>
<x:String x:Key="Text.Period.DaysAgo" xml:space="preserve">{0} 天前</x:String>
@ -523,7 +522,6 @@
<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.DoNothing" 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.Remote" xml:space="preserve">遠端:</x:String>

View file

@ -9,16 +9,17 @@ namespace SourceGit.ViewModels
get;
}
public Models.DealWithLocalChanges PreAction
public bool DiscardLocalChanges
{
get;
set;
} = Models.DealWithLocalChanges.DoNothing;
}
public Checkout(Repository repo, string branch)
{
_repo = repo;
Branch = branch;
DiscardLocalChanges = false;
View = new Views.Checkout() { DataContext = this };
}
@ -33,7 +34,12 @@ namespace SourceGit.ViewModels
var needPopStash = false;
if (changes > 0)
{
if (PreAction == Models.DealWithLocalChanges.StashAndReaply)
if (DiscardLocalChanges)
{
SetProgressDescription("Discard local changes ...");
Commands.Discard.All(_repo.FullPath, false);
}
else
{
SetProgressDescription("Stash local changes ...");
var succ = new Commands.Stash(_repo.FullPath).Push("CHECKOUT_AUTO_STASH");
@ -45,11 +51,6 @@ namespace SourceGit.ViewModels
needPopStash = true;
}
else if (PreAction == Models.DealWithLocalChanges.Discard)
{
SetProgressDescription("Discard local changes ...");
Commands.Discard.All(_repo.FullPath, false);
}
}
SetProgressDescription("Checkout branch ...");

View file

@ -9,16 +9,17 @@ namespace SourceGit.ViewModels
get;
}
public bool AutoStash
public bool DiscardLocalChanges
{
get => _autoStash;
set => SetProperty(ref _autoStash, value);
get;
set;
}
public CheckoutCommit(Repository repo, Models.Commit commit)
{
_repo = repo;
Commit = commit;
DiscardLocalChanges = false;
View = new Views.CheckoutCommit() { DataContext = this };
}
@ -33,7 +34,12 @@ namespace SourceGit.ViewModels
var needPopStash = false;
if (changes > 0)
{
if (AutoStash)
if (DiscardLocalChanges)
{
SetProgressDescription("Discard local changes ...");
Commands.Discard.All(_repo.FullPath, false);
}
else
{
SetProgressDescription("Stash local changes ...");
var succ = new Commands.Stash(_repo.FullPath).Push("CHECKOUT_AUTO_STASH");
@ -45,11 +51,6 @@ namespace SourceGit.ViewModels
needPopStash = true;
}
else
{
SetProgressDescription("Discard local changes ...");
Commands.Discard.All(_repo.FullPath, false);
}
}
SetProgressDescription("Checkout commit ...");
@ -67,6 +68,5 @@ namespace SourceGit.ViewModels
}
private readonly Repository _repo = null;
private bool _autoStash = true;
}
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
@ -171,7 +172,7 @@ namespace SourceGit.ViewModels
_searchChangeFilter = null;
_diffContext = null;
_viewRevisionFileContent = null;
_cancelToken = null;
_cancellationSource = null;
WebLinks.Clear();
_revisionFiles = null;
_revisionFileSearchSuggestion = null;
@ -589,32 +590,36 @@ namespace SourceGit.ViewModels
if (_commit == null)
return;
if (_cancellationSource is { IsCancellationRequested: false })
_cancellationSource.Cancel();
_cancellationSource = new CancellationTokenSource();
var token = _cancellationSource.Token;
Task.Run(() =>
{
var message = new Commands.QueryCommitFullMessage(_repo.FullPath, _commit.SHA).Result();
var links = ParseLinksInMessage(message);
Dispatcher.UIThread.Invoke(() => FullMessage = new Models.CommitFullMessage { Message = message, Links = links });
if (!token.IsCancellationRequested)
Dispatcher.UIThread.Invoke(() => FullMessage = new Models.CommitFullMessage { Message = message, Links = links });
});
Task.Run(() =>
{
var signInfo = new Commands.QueryCommitSignInfo(_repo.FullPath, _commit.SHA, !_repo.HasAllowedSignersFile).Result();
Dispatcher.UIThread.Invoke(() => SignInfo = signInfo);
if (!token.IsCancellationRequested)
Dispatcher.UIThread.Invoke(() => SignInfo = signInfo);
});
if (_cancelToken != null)
_cancelToken.Requested = true;
_cancelToken = new Commands.Command.CancelToken();
if (Preferences.Instance.ShowChildren)
{
Task.Run(() =>
{
var max = Preferences.Instance.MaxHistoryCommits;
var cmdChildren = new Commands.QueryCommitChildren(_repo.FullPath, _commit.SHA, max) { Cancel = _cancelToken };
var children = cmdChildren.Result();
if (!cmdChildren.Cancel.Requested)
var cmd = new Commands.QueryCommitChildren(_repo.FullPath, _commit.SHA, max) { CancellationToken = token };
var children = cmd.Result();
if (!token.IsCancellationRequested)
Dispatcher.UIThread.Post(() => Children = children);
});
}
@ -622,8 +627,8 @@ namespace SourceGit.ViewModels
Task.Run(() =>
{
var parent = _commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : _commit.Parents[0];
var cmdChanges = new Commands.CompareRevisions(_repo.FullPath, parent, _commit.SHA) { Cancel = _cancelToken };
var changes = cmdChanges.Result();
var cmd = new Commands.CompareRevisions(_repo.FullPath, parent, _commit.SHA) { CancellationToken = token };
var changes = cmd.Result();
var visible = changes;
if (!string.IsNullOrWhiteSpace(_searchChangeFilter))
{
@ -635,7 +640,7 @@ namespace SourceGit.ViewModels
}
}
if (!cmdChanges.Cancel.Requested)
if (!token.IsCancellationRequested)
{
Dispatcher.UIThread.Post(() =>
{
@ -809,14 +814,11 @@ namespace SourceGit.ViewModels
Task.Run(() =>
{
var files = new Commands.QueryRevisionFileNames(_repo.FullPath, sha).Result();
var filesList = new List<string>();
filesList.AddRange(files);
Dispatcher.UIThread.Invoke(() =>
{
if (sha == Commit.SHA)
{
_revisionFiles = filesList;
_revisionFiles = files;
if (!string.IsNullOrEmpty(_revisionFileSearchFilter))
CalcRevisionFileSearchSuggestion();
}
@ -873,7 +875,7 @@ namespace SourceGit.ViewModels
private string _searchChangeFilter = string.Empty;
private DiffContext _diffContext = null;
private object _viewRevisionFileContent = null;
private Commands.Command.CancelToken _cancelToken = null;
private CancellationTokenSource _cancellationSource = null;
private List<string> _revisionFiles = null;
private string _revisionFileSearchFilter = string.Empty;
private List<string> _revisionFileSearchSuggestion = null;

View file

@ -1,5 +1,26 @@
namespace SourceGit.ViewModels
{
public class ConflictSourceBranch
{
public string Name { get; private set; }
public string Head { get; private set; }
public Models.Commit Revision { get; private set; }
public ConflictSourceBranch(string name, string head, Models.Commit revision)
{
Name = name;
Head = head;
Revision = revision;
}
public ConflictSourceBranch(Repository repo, Models.Branch branch)
{
Name = branch.Name;
Head = branch.Head;
Revision = new Commands.QuerySingleCommit(repo.FullPath, branch.Head).Result() ?? new Models.Commit() { SHA = branch.Head };
}
}
public class Conflict
{
public object Theirs
@ -31,28 +52,32 @@
if (context is CherryPickInProgress cherryPick)
{
Theirs = cherryPick.Head;
Mine = repo.CurrentBranch;
Mine = new ConflictSourceBranch(repo, repo.CurrentBranch);
}
else if (context is RebaseInProgress rebase)
{
var b = repo.Branches.Find(x => x.IsLocal && x.Name == rebase.HeadName);
Theirs = (object)b ?? rebase.StoppedAt;
if (b != null)
Theirs = new ConflictSourceBranch(b.Name, b.Head, rebase.StoppedAt);
else
Theirs = new ConflictSourceBranch(rebase.HeadName, rebase.StoppedAt?.SHA ?? "----------", rebase.StoppedAt);
Mine = rebase.Onto;
}
else if (context is RevertInProgress revert)
{
Theirs = revert.Head;
Mine = repo.CurrentBranch;
Mine = new ConflictSourceBranch(repo, repo.CurrentBranch);
}
else if (context is MergeInProgress merge)
{
Theirs = merge.Source;
Mine = repo.CurrentBranch;
Mine = new ConflictSourceBranch(repo, repo.CurrentBranch);
}
else
{
Theirs = "Stash or Patch";
Mine = repo.CurrentBranch;
Mine = new ConflictSourceBranch(repo, repo.CurrentBranch);
}
}

View file

@ -19,11 +19,11 @@ namespace SourceGit.ViewModels
get;
}
public Models.DealWithLocalChanges PreAction
public bool DiscardLocalChanges
{
get;
set;
} = Models.DealWithLocalChanges.DoNothing;
}
public bool CheckoutAfterCreated
{
@ -47,6 +47,7 @@ namespace SourceGit.ViewModels
}
BasedOn = branch;
DiscardLocalChanges = false;
View = new Views.CreateBranch() { DataContext = this };
}
@ -56,6 +57,7 @@ namespace SourceGit.ViewModels
_baseOnRevision = commit.SHA;
BasedOn = commit;
DiscardLocalChanges = false;
View = new Views.CreateBranch() { DataContext = this };
}
@ -65,6 +67,7 @@ namespace SourceGit.ViewModels
_baseOnRevision = tag.SHA;
BasedOn = tag;
DiscardLocalChanges = false;
View = new Views.CreateBranch() { DataContext = this };
}
@ -98,7 +101,12 @@ namespace SourceGit.ViewModels
var needPopStash = false;
if (changes > 0)
{
if (PreAction == Models.DealWithLocalChanges.StashAndReaply)
if (DiscardLocalChanges)
{
SetProgressDescription("Discard local changes...");
Commands.Discard.All(_repo.FullPath, false);
}
else
{
SetProgressDescription("Stash local changes");
succ = new Commands.Stash(_repo.FullPath).Push("CREATE_BRANCH_AUTO_STASH");
@ -110,11 +118,6 @@ namespace SourceGit.ViewModels
needPopStash = true;
}
else if (PreAction == Models.DealWithLocalChanges.Discard)
{
SetProgressDescription("Discard local changes...");
Commands.Discard.All(_repo.FullPath, false);
}
}
SetProgressDescription($"Create new branch '{fixedName}'");

View file

@ -96,7 +96,7 @@ namespace SourceGit.ViewModels
foreach (var remote in remotes)
{
SetProgressDescription($"Pushing tag to remote {remote.Name} ...");
new Commands.Push(_repo.FullPath, remote.Name, _tagName, false).Exec();
new Commands.Push(_repo.FullPath, remote.Name, $"refs/tags/{_tagName}", false).Exec();
}
}

View file

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
namespace SourceGit.ViewModels
{
@ -13,7 +14,7 @@ namespace SourceGit.ViewModels
public ExecuteCustomAction(Repository repo, Models.CustomAction action)
{
_repo = repo;
_args = action.Arguments.Replace("${REPO}", _repo.FullPath);
_args = action.Arguments.Replace("${REPO}", GetWorkdir());
CustomAction = action;
View = new Views.ExecuteCustomAction() { DataContext = this };
}
@ -21,7 +22,7 @@ namespace SourceGit.ViewModels
public ExecuteCustomAction(Repository repo, Models.CustomAction action, Models.Branch branch)
{
_repo = repo;
_args = action.Arguments.Replace("${REPO}", _repo.FullPath).Replace("${BRANCH}", branch.FriendlyName);
_args = action.Arguments.Replace("${REPO}", GetWorkdir()).Replace("${BRANCH}", branch.FriendlyName);
CustomAction = action;
View = new Views.ExecuteCustomAction() { DataContext = this };
}
@ -29,7 +30,7 @@ namespace SourceGit.ViewModels
public ExecuteCustomAction(Repository repo, Models.CustomAction action, Models.Commit commit)
{
_repo = repo;
_args = action.Arguments.Replace("${REPO}", _repo.FullPath).Replace("${SHA}", commit.SHA);
_args = action.Arguments.Replace("${REPO}", GetWorkdir()).Replace("${SHA}", commit.SHA);
CustomAction = action;
View = new Views.ExecuteCustomAction() { DataContext = this };
}
@ -51,6 +52,11 @@ namespace SourceGit.ViewModels
});
}
private string GetWorkdir()
{
return OperatingSystem.IsWindows() ? _repo.FullPath.Replace("/", "\\") : _repo.FullPath;
}
private readonly Repository _repo = null;
private string _args = string.Empty;
}

View file

@ -148,14 +148,14 @@ namespace SourceGit.ViewModels
{
if (commits.Count == 0)
{
_repo.SearchResultSelectedCommit = null;
_repo.SelectedSearchedCommit = null;
DetailContext = null;
}
else if (commits.Count == 1)
{
var commit = (commits[0] as Models.Commit)!;
if (_repo.SearchResultSelectedCommit == null || _repo.SearchResultSelectedCommit.SHA != commit.SHA)
_repo.SearchResultSelectedCommit = _repo.SearchedCommits.Find(x => x.SHA == commit.SHA);
if (_repo.SelectedSearchedCommit == null || _repo.SelectedSearchedCommit.SHA != commit.SHA)
_repo.SelectedSearchedCommit = _repo.SearchedCommits.Find(x => x.SHA == commit.SHA);
AutoSelectedCommit = commit;
NavigationId = _navigationId + 1;
@ -173,7 +173,7 @@ namespace SourceGit.ViewModels
}
else if (commits.Count == 2)
{
_repo.SearchResultSelectedCommit = null;
_repo.SelectedSearchedCommit = null;
var end = commits[0] as Models.Commit;
var start = commits[1] as Models.Commit;
@ -181,7 +181,7 @@ namespace SourceGit.ViewModels
}
else
{
_repo.SearchResultSelectedCommit = null;
_repo.SelectedSearchedCommit = null;
DetailContext = commits.Count;
}
}
@ -599,7 +599,7 @@ namespace SourceGit.ViewModels
var head = _commits.Find(x => x.SHA == current.Head);
if (head == null)
{
_repo.SearchResultSelectedCommit = null;
_repo.SelectedSearchedCommit = null;
head = new Commands.QuerySingleCommit(_repo.FullPath, current.Head).Result();
if (head != null)
DetailContext = new RevisionCompare(_repo.FullPath, commit, head);
@ -694,12 +694,7 @@ namespace SourceGit.ViewModels
menu.Items.Add(archive);
menu.Items.Add(new MenuItem() { Header = "-" });
var actions = new List<Models.CustomAction>();
foreach (var action in _repo.Settings.CustomActions)
{
if (action.Scope == Models.CustomActionScope.Commit)
actions.Add(action);
}
var actions = _repo.GetCustomActions(Models.CustomActionScope.Commit);
if (actions.Count > 0)
{
var custom = new MenuItem();

View file

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Avalonia.Collections;
@ -66,9 +65,8 @@ namespace SourceGit.ViewModels
get => _defaultFontFamily;
set
{
var name = FixFontFamilyName(value);
if (SetProperty(ref _defaultFontFamily, name) && !_isLoading)
App.SetFonts(_defaultFontFamily, _monospaceFontFamily, _onlyUseMonoFontInEditor);
if (SetProperty(ref _defaultFontFamily, value) && !_isLoading)
App.SetFonts(value, _monospaceFontFamily, _onlyUseMonoFontInEditor);
}
}
@ -77,9 +75,8 @@ namespace SourceGit.ViewModels
get => _monospaceFontFamily;
set
{
var name = FixFontFamilyName(value);
if (SetProperty(ref _monospaceFontFamily, name) && !_isLoading)
App.SetFonts(_defaultFontFamily, _monospaceFontFamily, _onlyUseMonoFontInEditor);
if (SetProperty(ref _monospaceFontFamily, value) && !_isLoading)
App.SetFonts(_defaultFontFamily, value, _onlyUseMonoFontInEditor);
}
}
@ -342,6 +339,12 @@ namespace SourceGit.ViewModels
set;
} = [];
public AvaloniaList<Models.CustomAction> CustomActions
{
get;
set;
} = [];
public AvaloniaList<Models.OpenAIService> OpenAIServices
{
get;
@ -614,35 +617,6 @@ namespace SourceGit.ViewModels
return changed;
}
private string FixFontFamilyName(string name)
{
var trimmed = name.Trim();
if (string.IsNullOrEmpty(trimmed))
return string.Empty;
var builder = new StringBuilder();
var lastIsSpace = false;
for (int i = 0; i < trimmed.Length; i++)
{
var c = trimmed[i];
if (char.IsWhiteSpace(c))
{
if (lastIsSpace)
continue;
lastIsSpace = true;
}
else
{
lastIsSpace = false;
}
builder.Append(c);
}
return builder.ToString();
}
private static Preferences _instance = null;
private static bool _isLoading = false;

View file

@ -38,11 +38,11 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _selectedBranch, value, true);
}
public Models.DealWithLocalChanges PreAction
public bool DiscardLocalChanges
{
get;
set;
} = Models.DealWithLocalChanges.DoNothing;
}
public bool UseRebase
{
@ -124,7 +124,12 @@ namespace SourceGit.ViewModels
var needPopStash = false;
if (changes > 0)
{
if (PreAction == Models.DealWithLocalChanges.StashAndReaply)
if (DiscardLocalChanges)
{
SetProgressDescription("Discard local changes ...");
Commands.Discard.All(_repo.FullPath, false);
}
else
{
SetProgressDescription("Stash local changes...");
var succ = new Commands.Stash(_repo.FullPath).Push("PULL_AUTO_STASH");
@ -136,11 +141,6 @@ namespace SourceGit.ViewModels
needPopStash = true;
}
else if (PreAction == Models.DealWithLocalChanges.Discard)
{
SetProgressDescription("Discard local changes ...");
Commands.Discard.All(_repo.FullPath, false);
}
}
bool rs;

View file

@ -43,13 +43,14 @@ namespace SourceGit.ViewModels
return Task.Run(() =>
{
bool succ = true;
var succ = true;
var tag = $"refs/tags/{Target.Name}";
if (_pushAllRemotes)
{
foreach (var remote in _repo.Remotes)
{
SetProgressDescription($"Pushing tag to remote {remote.Name} ...");
succ = new Commands.Push(_repo.FullPath, remote.Name, Target.Name, false).Exec();
succ = new Commands.Push(_repo.FullPath, remote.Name, tag, false).Exec();
if (!succ)
break;
}
@ -57,7 +58,7 @@ namespace SourceGit.ViewModels
else
{
SetProgressDescription($"Pushing tag to remote {SelectedRemote.Name} ...");
succ = new Commands.Push(_repo.FullPath, SelectedRemote.Name, Target.Name, false).Exec();
succ = new Commands.Push(_repo.FullPath, SelectedRemote.Name, tag, false).Exec();
}
CallUIThread(() => _repo.SetWatcherEnabled(true));

View file

@ -12,7 +12,7 @@ namespace SourceGit.ViewModels
}
[Required(ErrorMessage = "Branch name is required!!!")]
[RegularExpression(@"^[\w\-/\.#]+$", ErrorMessage = "Bad branch name format!")]
[RegularExpression(@"^[\w \-/\.#]+$", ErrorMessage = "Bad branch name format!")]
[CustomValidation(typeof(RenameBranch), nameof(ValidateBranchName))]
public string Name
{
@ -32,9 +32,10 @@ namespace SourceGit.ViewModels
{
if (ctx.ObjectInstance is RenameBranch rename)
{
var fixedName = rename.FixName(name);
foreach (var b in rename._repo.Branches)
{
if (b.IsLocal && b != rename.Target && b.Name == name)
if (b.IsLocal && b != rename.Target && b.Name == fixedName)
{
return new ValidationResult("A branch with same name already exists!!!");
}
@ -46,7 +47,8 @@ namespace SourceGit.ViewModels
public override Task<bool> Sure()
{
if (_name == Target.Name)
var fixedName = FixName(_name);
if (fixedName == Target.Name)
return null;
_repo.SetWatcherEnabled(false);
@ -55,7 +57,7 @@ namespace SourceGit.ViewModels
return Task.Run(() =>
{
var oldName = Target.FullName;
var succ = Commands.Branch.Rename(_repo.FullPath, Target.Name, _name);
var succ = Commands.Branch.Rename(_repo.FullPath, Target.Name, fixedName);
CallUIThread(() =>
{
if (succ)
@ -65,7 +67,7 @@ namespace SourceGit.ViewModels
if (filter.Type == Models.FilterType.LocalBranch &&
filter.Pattern.Equals(oldName, StringComparison.Ordinal))
{
filter.Pattern = $"refs/heads/{_name}";
filter.Pattern = $"refs/heads/{fixedName}";
break;
}
}
@ -78,6 +80,15 @@ namespace SourceGit.ViewModels
});
}
private string FixName(string name)
{
if (!name.Contains(' '))
return name;
var parts = name.Split(' ', StringSplitOptions.RemoveEmptyEntries);
return string.Join("-", parts);
}
private readonly Repository _repo;
private string _name;
}

View file

@ -6,7 +6,6 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
@ -269,16 +268,18 @@ namespace SourceGit.ViewModels
{
if (SetProperty(ref _isSearching, value))
{
SearchedCommits = new List<Models.Commit>();
SearchCommitFilter = string.Empty;
SearchCommitFilterSuggestion.Clear();
IsSearchCommitSuggestionOpen = false;
_revisionFiles.Clear();
if (value)
{
SelectedViewIndex = 0;
UpdateCurrentRevisionFilesForSearchSuggestion();
CalcWorktreeFilesForSearching();
}
else
{
SearchedCommits = new List<Models.Commit>();
SelectedSearchedCommit = null;
SearchCommitFilter = string.Empty;
MatchedFilesForSearching = null;
_worktreeFiles = null;
}
}
}
@ -307,8 +308,7 @@ namespace SourceGit.ViewModels
{
if (SetProperty(ref _searchCommitFilterType, value))
{
UpdateCurrentRevisionFilesForSearchSuggestion();
CalcWorktreeFilesForSearching();
if (!string.IsNullOrEmpty(_searchCommitFilter))
StartSearchCommits();
}
@ -320,53 +320,33 @@ namespace SourceGit.ViewModels
get => _searchCommitFilter;
set
{
if (SetProperty(ref _searchCommitFilter, value) &&
_searchCommitFilterType == 3 &&
!string.IsNullOrEmpty(value) &&
value.Length >= 2 &&
_revisionFiles.Count > 0)
{
var suggestion = new List<string>();
foreach (var file in _revisionFiles)
{
if (file.Contains(value, StringComparison.OrdinalIgnoreCase) && file.Length != value.Length)
{
suggestion.Add(file);
if (suggestion.Count > 100)
break;
}
}
SearchCommitFilterSuggestion.Clear();
SearchCommitFilterSuggestion.AddRange(suggestion);
IsSearchCommitSuggestionOpen = SearchCommitFilterSuggestion.Count > 0;
}
else if (SearchCommitFilterSuggestion.Count > 0)
{
SearchCommitFilterSuggestion.Clear();
IsSearchCommitSuggestionOpen = false;
}
if (SetProperty(ref _searchCommitFilter, value) && IsSearchingCommitsByFilePath())
CalcMatchedFilesForSearching();
}
}
public bool IsSearchCommitSuggestionOpen
public List<string> MatchedFilesForSearching
{
get => _isSearchCommitSuggestionOpen;
set => SetProperty(ref _isSearchCommitSuggestionOpen, value);
get => _matchedFilesForSearching;
private set => SetProperty(ref _matchedFilesForSearching, value);
}
public AvaloniaList<string> SearchCommitFilterSuggestion
{
get;
private set;
} = new AvaloniaList<string>();
public List<Models.Commit> SearchedCommits
{
get => _searchedCommits;
set => SetProperty(ref _searchedCommits, value);
}
public Models.Commit SelectedSearchedCommit
{
get => _selectedSearchedCommit;
set
{
if (SetProperty(ref _selectedSearchedCommit, value) && value != null)
NavigateToCommit(value.SHA);
}
}
public bool IsLocalBranchGroupExpanded
{
get => _settings.IsLocalBranchesExpandedInSideBar;
@ -437,16 +417,6 @@ namespace SourceGit.ViewModels
get => _workingCopy?.InProgressContext;
}
public Models.Commit SearchResultSelectedCommit
{
get => _searchResultSelectedCommit;
set
{
if (SetProperty(ref _searchResultSelectedCommit, value) && value != null)
NavigateToCommit(value.SHA);
}
}
public bool IsAutoFetching
{
get => _isAutoFetching;
@ -518,7 +488,7 @@ namespace SourceGit.ViewModels
{
File.WriteAllText(Path.Combine(_gitDir, "sourcegit.settings"), settingsSerialized);
}
catch (DirectoryNotFoundException)
catch
{
// Ignore
}
@ -550,9 +520,10 @@ namespace SourceGit.ViewModels
_submodules.Clear();
_visibleSubmodules.Clear();
_searchedCommits.Clear();
_selectedSearchedCommit = null;
_revisionFiles.Clear();
SearchCommitFilterSuggestion.Clear();
_worktreeFiles = null;
_matchedFilesForSearching = null;
}
public bool CanCreatePopup()
@ -723,39 +694,33 @@ namespace SourceGit.ViewModels
SearchCommitFilter = string.Empty;
}
public void ClearMatchedFilesForSearching()
{
MatchedFilesForSearching = null;
}
public void StartSearchCommits()
{
if (_histories == null)
return;
IsSearchLoadingVisible = true;
SearchResultSelectedCommit = null;
IsSearchCommitSuggestionOpen = false;
SearchCommitFilterSuggestion.Clear();
SelectedSearchedCommit = null;
MatchedFilesForSearching = null;
Task.Run(() =>
{
var visible = new List<Models.Commit>();
var visible = null as List<Models.Commit>;
var method = (Models.CommitSearchMethod)_searchCommitFilterType;
switch (_searchCommitFilterType)
if (method == Models.CommitSearchMethod.BySHA)
{
case 0:
var commit = new Commands.QuerySingleCommit(_fullpath, _searchCommitFilter).Result();
if (commit != null)
visible.Add(commit);
break;
case 1:
visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByAuthor, _onlySearchCommitsInCurrentBranch).Result();
break;
case 2:
visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByCommitter, _onlySearchCommitsInCurrentBranch).Result();
break;
case 3:
visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByMessage, _onlySearchCommitsInCurrentBranch).Result();
break;
case 4:
visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByFile, _onlySearchCommitsInCurrentBranch).Result();
break;
var commit = new Commands.QuerySingleCommit(_fullpath, _searchCommitFilter).Result();
visible = commit == null ? [] : [commit];
}
else
{
visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, method, _onlySearchCommitsInCurrentBranch).Result();
}
Dispatcher.UIThread.Invoke(() =>
@ -943,6 +908,25 @@ namespace SourceGit.ViewModels
_workingCopy?.AbortMerge();
}
public List<Models.CustomAction> GetCustomActions(Models.CustomActionScope scope)
{
var actions = new List<Models.CustomAction>();
foreach (var act in Preferences.Instance.CustomActions)
{
if (act.Scope == scope)
actions.Add(act);
}
foreach (var act in _settings.CustomActions)
{
if (act.Scope == scope)
actions.Add(act);
}
return actions;
}
public void RefreshBranches()
{
var branches = new Commands.QueryBranches(_fullpath).Result();
@ -1224,23 +1208,26 @@ namespace SourceGit.ViewModels
App.GetLauncer()?.OpenRepositoryInTab(node, null);
}
public AvaloniaList<Models.OpenAIService> GetPreferedOpenAIServices()
public List<Models.OpenAIService> GetPreferedOpenAIServices()
{
var services = Preferences.Instance.OpenAIServices;
if (services == null || services.Count == 0)
return [];
if (services.Count == 1)
return services;
return [services[0]];
var prefered = _settings.PreferedOpenAIService;
var all = new List<Models.OpenAIService>();
foreach (var service in services)
{
if (service.Name.Equals(prefered, StringComparison.Ordinal))
return [service];
all.Add(service);
}
return services;
return all;
}
public ContextMenu CreateContextMenuForGitFlow()
@ -1443,16 +1430,10 @@ namespace SourceGit.ViewModels
public ContextMenu CreateContextMenuForCustomAction()
{
var actions = new List<Models.CustomAction>();
foreach (var action in _settings.CustomActions)
{
if (action.Scope == Models.CustomActionScope.Repository)
actions.Add(action);
}
var menu = new ContextMenu();
menu.Placement = PlacementMode.BottomEdgeAlignedLeft;
var actions = GetCustomActions(Models.CustomActionScope.Repository);
if (actions.Count > 0)
{
foreach (var action in actions)
@ -1643,7 +1624,7 @@ namespace SourceGit.ViewModels
compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare");
compareWithWorktree.Click += (_, _) =>
{
SearchResultSelectedCommit = null;
SelectedSearchedCommit = null;
if (_histories != null)
{
@ -1925,7 +1906,7 @@ namespace SourceGit.ViewModels
compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare");
compareWithWorktree.Click += (_, _) =>
{
SearchResultSelectedCommit = null;
SelectedSearchedCommit = null;
if (_histories != null)
{
@ -2349,13 +2330,7 @@ namespace SourceGit.ViewModels
private void TryToAddCustomActionsToBranchContextMenu(ContextMenu menu, Models.Branch branch)
{
var actions = new List<Models.CustomAction>();
foreach (var action in Settings.CustomActions)
{
if (action.Scope == Models.CustomActionScope.Branch)
actions.Add(action);
}
var actions = GetCustomActions(Models.CustomActionScope.Branch);
if (actions.Count == 0)
return;
@ -2384,42 +2359,52 @@ namespace SourceGit.ViewModels
menu.Items.Add(new MenuItem() { Header = "-" });
}
private void UpdateCurrentRevisionFilesForSearchSuggestion()
private bool IsSearchingCommitsByFilePath()
{
_revisionFiles.Clear();
return _isSearching && _searchCommitFilterType == (int)Models.CommitSearchMethod.ByFile;
}
if (_searchCommitFilterType == 3)
private void CalcWorktreeFilesForSearching()
{
if (!IsSearchingCommitsByFilePath())
{
Task.Run(() =>
{
var files = new Commands.QueryRevisionFileNames(_fullpath, "HEAD").Result();
Dispatcher.UIThread.Invoke(() =>
{
if (_searchCommitFilterType != 3)
return;
_revisionFiles.AddRange(files);
if (!string.IsNullOrEmpty(_searchCommitFilter) && _searchCommitFilter.Length > 2 && _revisionFiles.Count > 0)
{
var suggestion = new List<string>();
foreach (var file in _revisionFiles)
{
if (file.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase) && file.Length != _searchCommitFilter.Length)
{
suggestion.Add(file);
if (suggestion.Count > 100)
break;
}
}
SearchCommitFilterSuggestion.Clear();
SearchCommitFilterSuggestion.AddRange(suggestion);
IsSearchCommitSuggestionOpen = SearchCommitFilterSuggestion.Count > 0;
}
});
});
_worktreeFiles = null;
MatchedFilesForSearching = null;
GC.Collect();
return;
}
Task.Run(() =>
{
_worktreeFiles = new Commands.QueryRevisionFileNames(_fullpath, "HEAD").Result();
Dispatcher.UIThread.Invoke(() =>
{
if (IsSearchingCommitsByFilePath())
CalcMatchedFilesForSearching();
});
});
}
private void CalcMatchedFilesForSearching()
{
if (_worktreeFiles == null || _worktreeFiles.Count == 0 || _searchCommitFilter.Length < 3)
{
MatchedFilesForSearching = null;
return;
}
var matched = new List<string>();
foreach (var file in _worktreeFiles)
{
if (file.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase) && file.Length != _searchCommitFilter.Length)
{
matched.Add(file);
if (matched.Count > 100)
break;
}
}
MatchedFilesForSearching = matched;
}
private void AutoFetchImpl(object sender)
@ -2468,13 +2453,13 @@ namespace SourceGit.ViewModels
private bool _isSearching = false;
private bool _isSearchLoadingVisible = false;
private bool _isSearchCommitSuggestionOpen = false;
private int _searchCommitFilterType = 3;
private int _searchCommitFilterType = (int)Models.CommitSearchMethod.ByMessage;
private bool _onlySearchCommitsInCurrentBranch = false;
private string _searchCommitFilter = string.Empty;
private List<Models.Commit> _searchedCommits = new List<Models.Commit>();
private Models.Commit _searchResultSelectedCommit = null;
private List<string> _revisionFiles = new List<string>();
private Models.Commit _selectedSearchedCommit = null;
private List<string> _worktreeFiles = null;
private List<string> _matchedFilesForSearching = null;
private string _filter = string.Empty;
private object _lockRemotes = new object();

View file

@ -1452,28 +1452,24 @@ namespace SourceGit.ViewModels
App.OpenDialog(dialog);
return null;
}
else
var menu = new ContextMenu() { Placement = PlacementMode.TopEdgeAlignedLeft };
foreach (var service in services)
{
var menu = new ContextMenu() { Placement = PlacementMode.TopEdgeAlignedLeft };
foreach (var service in services)
var dup = service;
var item = new MenuItem();
item.Header = service.Name;
item.Click += (_, e) =>
{
var dup = service;
var dialog = new Views.AIAssistant(dup, _repo.FullPath, this, _staged);
App.OpenDialog(dialog);
e.Handled = true;
};
var item = new MenuItem();
item.Header = service.Name;
item.Click += (_, e) =>
{
var dialog = new Views.AIAssistant(dup, _repo.FullPath, this, _staged);
App.OpenDialog(dialog);
e.Handled = true;
};
menu.Items.Add(item);
}
return menu;
menu.Items.Add(item);
}
return menu;
}
private List<Models.Change> GetVisibleUnstagedChanges(List<Models.Change> unstaged)

View file

@ -39,7 +39,8 @@
<Grid ColumnDefinitions="16,Auto,Auto,*"
Margin="{Binding Depth, Converter={x:Static c:IntConverters.ToTreeMargin}}"
Background="Transparent"
DoubleTapped="OnRowDoubleTapped">
DoubleTapped="OnRowDoubleTapped"
ToolTip.Tip="{Binding FullPath}">
<v:ChangeTreeNodeToggleButton Grid.Column="0"
Classes="tree_expander"
Focusable="False"
@ -71,7 +72,10 @@
SelectionChanged="OnRowSelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Change">
<Grid ColumnDefinitions="Auto,Auto,Auto,*" Background="Transparent" DoubleTapped="OnRowDoubleTapped">
<Grid ColumnDefinitions="Auto,Auto,Auto,*"
Background="Transparent"
DoubleTapped="OnRowDoubleTapped"
ToolTip.Tip="{Binding Path}">
<v:ChangeStatusIcon Grid.Column="0"
Width="14" Height="14"
Margin="4,0,0,0"
@ -100,7 +104,10 @@
SelectionChanged="OnRowSelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Change">
<Grid ColumnDefinitions="Auto,Auto,*" Background="Transparent" DoubleTapped="OnRowDoubleTapped">
<Grid ColumnDefinitions="Auto,Auto,*"
Background="Transparent"
DoubleTapped="OnRowDoubleTapped"
ToolTip.Tip="{Binding Path}">
<v:ChangeStatusIcon Grid.Column="0"
Width="14" Height="14"
Margin="4,0,0,0"

View file

@ -33,20 +33,12 @@
Margin="0,0,8,0"
Text="{DynamicResource Text.Checkout.LocalChanges}"/>
<WrapPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<RadioButton Content="{DynamicResource Text.CreateBranch.LocalChanges.DoNothing}"
x:Name="RadioDoNothing"
GroupName="LocalChanges"
<RadioButton GroupName="LocalChanges"
Margin="0,0,8,0"
IsCheckedChanged="OnLocalChangeActionIsCheckedChanged"/>
<RadioButton Content="{DynamicResource Text.CreateBranch.LocalChanges.StashAndReply}"
x:Name="RadioStashAndReply"
GroupName="LocalChanges"
Margin="0,0,8,0"
IsCheckedChanged="OnLocalChangeActionIsCheckedChanged"/>
<RadioButton Content="{DynamicResource Text.CreateBranch.LocalChanges.Discard}"
x:Name="RadioDiscard"
GroupName="LocalChanges"
IsCheckedChanged="OnLocalChangeActionIsCheckedChanged"/>
Content="{DynamicResource Text.CreateBranch.LocalChanges.StashAndReply}"
IsChecked="{Binding !DiscardLocalChanges, Mode=TwoWay}"/>
<RadioButton GroupName="LocalChanges"
Content="{DynamicResource Text.CreateBranch.LocalChanges.Discard}"/>
</WrapPanel>
</Grid>
</StackPanel>

View file

@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace SourceGit.Views
{
@ -9,51 +8,5 @@ namespace SourceGit.Views
{
InitializeComponent();
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
var vm = DataContext as ViewModels.Checkout;
if (vm == null)
return;
switch (vm.PreAction)
{
case Models.DealWithLocalChanges.DoNothing:
RadioDoNothing.IsChecked = true;
break;
case Models.DealWithLocalChanges.StashAndReaply:
RadioStashAndReply.IsChecked = true;
break;
default:
RadioDiscard.IsChecked = true;
break;
}
}
private void OnLocalChangeActionIsCheckedChanged(object sender, RoutedEventArgs e)
{
var vm = DataContext as ViewModels.Checkout;
if (vm == null)
return;
if (RadioDoNothing.IsChecked == true)
{
if (vm.PreAction != Models.DealWithLocalChanges.DoNothing)
vm.PreAction = Models.DealWithLocalChanges.DoNothing;
return;
}
if (RadioStashAndReply.IsChecked == true)
{
if (vm.PreAction != Models.DealWithLocalChanges.StashAndReaply)
vm.PreAction = Models.DealWithLocalChanges.StashAndReaply;
return;
}
if (vm.PreAction != Models.DealWithLocalChanges.Discard)
vm.PreAction = Models.DealWithLocalChanges.Discard;
}
}
}

View file

@ -30,10 +30,10 @@
<StackPanel Grid.Row="1" Grid.Column="1" Height="32" Orientation="Horizontal">
<RadioButton Content="{DynamicResource Text.Checkout.LocalChanges.StashAndReply}"
GroupName="LocalChanges"
IsChecked="{Binding AutoStash, Mode=TwoWay}" />
Margin="0,0,8,0"
IsChecked="{Binding !DiscardLocalChanges, Mode=TwoWay}"/>
<RadioButton Content="{DynamicResource Text.Checkout.LocalChanges.Discard}"
GroupName="LocalChanges"
Margin="8,0,0,0" />
GroupName="LocalChanges"/>
</StackPanel>
<Grid Grid.Row="2" Grid.Column="1" ColumnDefinitions="Auto,*" Margin="0,6,0,0">

View file

@ -80,10 +80,10 @@
<StackPanel Orientation="Vertical">
<TextBlock Margin="0,0,0,12" Text="{Binding ToolTip}"/>
<Grid ColumnDefinitions="Auto,8,Auto" RowDefinitions="Auto,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" Text="Signer:" IsVisible="{Binding HasSigner}"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding Signer}" IsVisible="{Binding HasSigner}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Key:"/>
<TextBlock Grid.Row="1" Grid.Column="2" Text="{Binding Key}"/>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Signer:" IsVisible="{Binding HasSigner}" VerticalAlignment="Center"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding Signer}" IsVisible="{Binding HasSigner}" VerticalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Key:" VerticalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="2" Text="{Binding Key}" VerticalAlignment="Center"/>
</Grid>
</StackPanel>
</ToolTip.Tip>

228
src/Views/CommitGraph.cs Normal file
View file

@ -0,0 +1,228 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace SourceGit.Views
{
public class CommitGraph : Control
{
public static readonly StyledProperty<Models.CommitGraph> GraphProperty =
AvaloniaProperty.Register<CommitGraph, Models.CommitGraph>(nameof(Graph));
public Models.CommitGraph Graph
{
get => GetValue(GraphProperty);
set => SetValue(GraphProperty, value);
}
public static readonly StyledProperty<IBrush> DotBrushProperty =
AvaloniaProperty.Register<CommitGraph, IBrush>(nameof(DotBrush), Brushes.Transparent);
public IBrush DotBrush
{
get => GetValue(DotBrushProperty);
set => SetValue(DotBrushProperty, value);
}
public static readonly StyledProperty<bool> OnlyHighlightCurrentBranchProperty =
AvaloniaProperty.Register<CommitGraph, bool>(nameof(OnlyHighlightCurrentBranch), true);
public bool OnlyHighlightCurrentBranch
{
get => GetValue(OnlyHighlightCurrentBranchProperty);
set => SetValue(OnlyHighlightCurrentBranchProperty, value);
}
static CommitGraph()
{
AffectsRender<CommitGraph>(GraphProperty, DotBrushProperty, OnlyHighlightCurrentBranchProperty);
}
public override void Render(DrawingContext context)
{
base.Render(context);
var graph = Graph;
if (graph == null)
return;
var histories = this.FindAncestorOfType<Histories>();
if (histories == null)
return;
var list = histories.CommitListContainer;
if (list == null)
return;
// Calculate drawing area.
double width = Bounds.Width - 273 - histories.AuthorNameColumnWidth.Value;
double height = Bounds.Height;
double startY = list.Scroll?.Offset.Y ?? 0;
double endY = startY + height + 28;
// Apply scroll offset and clip.
using (context.PushClip(new Rect(0, 0, width, height)))
using (context.PushTransform(Matrix.CreateTranslation(0, -startY)))
{
// Draw contents
DrawCurves(context, graph, startY, endY);
DrawAnchors(context, graph, startY, endY);
}
}
private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double top, double bottom)
{
var grayedPen = new Pen(new SolidColorBrush(Colors.Gray, 0.4), Models.CommitGraph.Pens[0].Thickness);
var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch;
if (onlyHighlightCurrentBranch)
{
foreach (var link in graph.Links)
{
if (link.IsMerged)
continue;
if (link.End.Y < top)
continue;
if (link.Start.Y > bottom)
break;
var geo = new StreamGeometry();
using (var ctx = geo.Open())
{
ctx.BeginFigure(link.Start, false);
ctx.QuadraticBezierTo(link.Control, link.End);
}
context.DrawGeometry(null, grayedPen, geo);
}
}
foreach (var line in graph.Paths)
{
var last = line.Points[0];
var size = line.Points.Count;
if (line.Points[size - 1].Y < top)
continue;
if (last.Y > bottom)
break;
var geo = new StreamGeometry();
var pen = Models.CommitGraph.Pens[line.Color];
using (var ctx = geo.Open())
{
var started = false;
var ended = false;
for (int i = 1; i < size; i++)
{
var cur = line.Points[i];
if (cur.Y < top)
{
last = cur;
continue;
}
if (!started)
{
ctx.BeginFigure(last, false);
started = true;
}
if (cur.Y > bottom)
{
cur = new Point(cur.X, bottom);
ended = true;
}
if (cur.X > last.X)
{
ctx.QuadraticBezierTo(new Point(cur.X, last.Y), cur);
}
else if (cur.X < last.X)
{
if (i < size - 1)
{
var midY = (last.Y + cur.Y) / 2;
ctx.CubicBezierTo(new Point(last.X, midY + 4), new Point(cur.X, midY - 4), cur);
}
else
{
ctx.QuadraticBezierTo(new Point(last.X, cur.Y), cur);
}
}
else
{
ctx.LineTo(cur);
}
if (ended)
break;
last = cur;
}
}
if (!line.IsMerged && onlyHighlightCurrentBranch)
context.DrawGeometry(null, grayedPen, geo);
else
context.DrawGeometry(null, pen, geo);
}
foreach (var link in graph.Links)
{
if (onlyHighlightCurrentBranch && !link.IsMerged)
continue;
if (link.End.Y < top)
continue;
if (link.Start.Y > bottom)
break;
var geo = new StreamGeometry();
using (var ctx = geo.Open())
{
ctx.BeginFigure(link.Start, false);
ctx.QuadraticBezierTo(link.Control, link.End);
}
context.DrawGeometry(null, Models.CommitGraph.Pens[link.Color], geo);
}
}
private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, double top, double bottom)
{
var dotFill = DotBrush;
var dotFillPen = new Pen(dotFill, 2);
var grayedPen = new Pen(Brushes.Gray, Models.CommitGraph.Pens[0].Thickness);
var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch;
foreach (var dot in graph.Dots)
{
if (dot.Center.Y < top)
continue;
if (dot.Center.Y > bottom)
break;
var pen = Models.CommitGraph.Pens[dot.Color];
if (!dot.IsMerged && onlyHighlightCurrentBranch)
pen = grayedPen;
switch (dot.Type)
{
case Models.CommitGraph.DotType.Head:
context.DrawEllipse(dotFill, pen, dot.Center, 6, 6);
context.DrawEllipse(pen.Brush, null, dot.Center, 3, 3);
break;
case Models.CommitGraph.DotType.Merge:
context.DrawEllipse(pen.Brush, null, dot.Center, 6, 6);
context.DrawLine(dotFillPen, new Point(dot.Center.X, dot.Center.Y - 3), new Point(dot.Center.X, dot.Center.Y + 3));
context.DrawLine(dotFillPen, new Point(dot.Center.X - 3, dot.Center.Y), new Point(dot.Center.X + 3, dot.Center.Y));
break;
default:
context.DrawEllipse(dotFill, pen, dot.Center, 3, 3);
break;
}
}
}
}
}

View file

@ -0,0 +1,90 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
namespace SourceGit.Views
{
public class CommitStatusIndicator : Control
{
public static readonly StyledProperty<Models.Branch> CurrentBranchProperty =
AvaloniaProperty.Register<CommitStatusIndicator, Models.Branch>(nameof(CurrentBranch));
public Models.Branch CurrentBranch
{
get => GetValue(CurrentBranchProperty);
set => SetValue(CurrentBranchProperty, value);
}
public static readonly StyledProperty<IBrush> AheadBrushProperty =
AvaloniaProperty.Register<CommitStatusIndicator, IBrush>(nameof(AheadBrush));
public IBrush AheadBrush
{
get => GetValue(AheadBrushProperty);
set => SetValue(AheadBrushProperty, value);
}
public static readonly StyledProperty<IBrush> BehindBrushProperty =
AvaloniaProperty.Register<CommitStatusIndicator, IBrush>(nameof(BehindBrush));
public IBrush BehindBrush
{
get => GetValue(BehindBrushProperty);
set => SetValue(BehindBrushProperty, value);
}
enum Status
{
Normal,
Ahead,
Behind,
}
public override void Render(DrawingContext context)
{
if (_status == Status.Normal)
return;
context.DrawEllipse(_status == Status.Ahead ? AheadBrush : BehindBrush, null, new Rect(0, 0, 5, 5));
}
protected override Size MeasureOverride(Size availableSize)
{
if (DataContext is Models.Commit commit && CurrentBranch is not null)
{
var sha = commit.SHA;
var track = CurrentBranch.TrackStatus;
if (track.Ahead.Contains(sha))
_status = Status.Ahead;
else if (track.Behind.Contains(sha))
_status = Status.Behind;
else
_status = Status.Normal;
}
else
{
_status = Status.Normal;
}
return _status == Status.Normal ? new Size(0, 0) : new Size(9, 5);
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
InvalidateMeasure();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == CurrentBranchProperty)
InvalidateMeasure();
}
private Status _status = Status.Normal;
}
}

View file

@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
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 static readonly StyledProperty<string> SubjectProperty =
AvaloniaProperty.Register<CommitSubjectPresenter, string>(nameof(Subject));
public string Subject
{
get => GetValue(SubjectProperty);
set => SetValue(SubjectProperty, value);
}
public static readonly StyledProperty<AvaloniaList<Models.IssueTrackerRule>> IssueTrackerRulesProperty =
AvaloniaProperty.Register<CommitSubjectPresenter, AvaloniaList<Models.IssueTrackerRule>>(nameof(IssueTrackerRules));
public AvaloniaList<Models.IssueTrackerRule> IssueTrackerRules
{
get => GetValue(IssueTrackerRulesProperty);
set => SetValue(IssueTrackerRulesProperty, value);
}
protected override Type StyleKeyOverride => typeof(TextBlock);
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == SubjectProperty || change.Property == IssueTrackerRulesProperty)
{
Inlines!.Clear();
_matches = null;
ClearHoveredIssueLink();
var subject = Subject;
if (string.IsNullOrEmpty(subject))
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;
}
matches.Sort((l, r) => l.Start - r.Start);
_matches = matches;
var inlines = new List<Inline>();
var pos = 0;
foreach (var match in matches)
{
if (match.Start > pos)
{
if (keywordMatch.Success && pos < keywordMatch.Length)
{
if (keywordMatch.Length < match.Start)
{
inlines.Add(new Run(subject.Substring(pos, keywordMatch.Length - pos)) { FontWeight = FontWeight.Bold });
inlines.Add(new Run(subject.Substring(keywordMatch.Length, match.Start - keywordMatch.Length)));
}
else
{
inlines.Add(new Run(subject.Substring(pos, match.Start - pos)) { FontWeight = FontWeight.Bold });
}
}
else
{
inlines.Add(new Run(subject.Substring(pos, match.Start - pos)));
}
}
var link = new Run(subject.Substring(match.Start, match.Length));
link.Classes.Add("issue_link");
inlines.Add(link);
pos = match.Start + match.Length;
}
if (pos < subject.Length)
{
if (keywordMatch.Success && pos < keywordMatch.Length)
{
inlines.Add(new Run(subject.Substring(pos, keywordMatch.Length - pos)) { FontWeight = FontWeight.Bold });
inlines.Add(new Run(subject.Substring(keywordMatch.Length)));
}
else
{
inlines.Add(new Run(subject.Substring(pos)));
}
}
Inlines.AddRange(inlines);
}
}
protected override void OnPointerMoved(PointerEventArgs e)
{
base.OnPointerMoved(e);
if (_matches != null)
{
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))
continue;
if (match == _lastHover)
return;
_lastHover = match;
SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
ToolTip.SetTip(this, match.Link);
ToolTip.SetIsOpen(this, true);
e.Handled = true;
return;
}
ClearHoveredIssueLink();
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if (_lastHover != null)
Native.OS.OpenBrowser(_lastHover.Link);
}
protected override void OnPointerExited(PointerEventArgs e)
{
base.OnPointerExited(e);
ClearHoveredIssueLink();
}
private void ClearHoveredIssueLink()
{
if (_lastHover != null)
{
ToolTip.SetTip(this, null);
SetCurrentValue(CursorProperty, Cursor.Parse("Arrow"));
_lastHover = null;
}
}
[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;
}
}

View file

@ -0,0 +1,166 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Threading;
namespace SourceGit.Views
{
public class CommitTimeTextBlock : TextBlock
{
public static readonly StyledProperty<bool> ShowAsDateTimeProperty =
AvaloniaProperty.Register<CommitTimeTextBlock, bool>(nameof(ShowAsDateTime), true);
public bool ShowAsDateTime
{
get => GetValue(ShowAsDateTimeProperty);
set => SetValue(ShowAsDateTimeProperty, value);
}
public static readonly StyledProperty<int> DateTimeFormatProperty =
AvaloniaProperty.Register<CommitTimeTextBlock, int>(nameof(DateTimeFormat), 0);
public int DateTimeFormat
{
get => GetValue(DateTimeFormatProperty);
set => SetValue(DateTimeFormatProperty, value);
}
public static readonly StyledProperty<bool> UseAuthorTimeProperty =
AvaloniaProperty.Register<CommitTimeTextBlock, bool>(nameof(UseAuthorTime), true);
public bool UseAuthorTime
{
get => GetValue(UseAuthorTimeProperty);
set => SetValue(UseAuthorTimeProperty, value);
}
protected override Type StyleKeyOverride => typeof(TextBlock);
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == UseAuthorTimeProperty)
{
SetCurrentValue(TextProperty, GetDisplayText());
}
else if (change.Property == ShowAsDateTimeProperty)
{
SetCurrentValue(TextProperty, GetDisplayText());
if (ShowAsDateTime)
StopTimer();
else
StartTimer();
}
else if (change.Property == DateTimeFormatProperty)
{
if (ShowAsDateTime)
SetCurrentValue(TextProperty, GetDisplayText());
}
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
if (!ShowAsDateTime)
StartTimer();
}
protected override void OnUnloaded(RoutedEventArgs e)
{
base.OnUnloaded(e);
StopTimer();
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
SetCurrentValue(TextProperty, GetDisplayText());
}
private void StartTimer()
{
if (_refreshTimer != null)
return;
_refreshTimer = DispatcherTimer.Run(() =>
{
Dispatcher.UIThread.Invoke(() =>
{
var text = GetDisplayText();
if (!text.Equals(Text, StringComparison.Ordinal))
Text = text;
});
return true;
}, TimeSpan.FromSeconds(10));
}
private void StopTimer()
{
if (_refreshTimer != null)
{
_refreshTimer.Dispose();
_refreshTimer = null;
}
}
private string GetDisplayText()
{
var commit = DataContext as Models.Commit;
if (commit == null)
return string.Empty;
if (ShowAsDateTime)
return UseAuthorTime ? commit.AuthorTimeStr : commit.CommitterTimeStr;
var timestamp = UseAuthorTime ? commit.AuthorTime : commit.CommitterTime;
var now = DateTime.Now;
var localTime = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime();
var span = now - localTime;
if (span.TotalMinutes < 1)
return App.Text("Period.JustNow");
if (span.TotalHours < 1)
return App.Text("Period.MinutesAgo", (int)span.TotalMinutes);
if (span.TotalDays < 1)
{
var hours = (int)span.TotalHours;
return hours == 1 ? App.Text("Period.HourAgo") : App.Text("Period.HoursAgo", hours);
}
var lastDay = now.AddDays(-1).Date;
if (localTime >= lastDay)
return App.Text("Period.Yesterday");
if ((localTime.Year == now.Year && localTime.Month == now.Month) || span.TotalDays < 28)
{
var diffDay = now.Date - localTime.Date;
return App.Text("Period.DaysAgo", (int)diffDay.TotalDays);
}
var lastMonth = now.AddMonths(-1).Date;
if (localTime.Year == lastMonth.Year && localTime.Month == lastMonth.Month)
return App.Text("Period.LastMonth");
if (localTime.Year == now.Year || localTime > now.AddMonths(-11))
{
var diffMonth = (12 + now.Month - localTime.Month) % 12;
return App.Text("Period.MonthsAgo", diffMonth);
}
var diffYear = now.Year - localTime.Year;
if (diffYear == 1)
return App.Text("Period.LastYear");
return App.Text("Period.YearsAgo", diffYear);
}
private IDisposable _refreshTimer = null;
}
}

View file

@ -11,8 +11,10 @@ namespace SourceGit.Views
protected override void OnClosing(WindowClosingEventArgs e)
{
ViewModels.Preferences.Instance.Save();
base.OnClosing(e);
if (!Design.IsDesignMode)
ViewModels.Preferences.Instance.Save();
}
}
}

122
src/Views/Conflict.axaml Normal file
View file

@ -0,0 +1,122 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:SourceGit.Models"
xmlns:v="using:SourceGit.Views"
xmlns:vm="using:SourceGit.ViewModels"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.Conflict"
x:DataType="vm:Conflict">
<Border Background="{DynamicResource Brush.Window}" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}">
<Grid VerticalAlignment="Center">
<StackPanel Orientation="Vertical" IsVisible="{Binding !IsResolved}">
<StackPanel.DataTemplates>
<DataTemplate DataType="vm:ConflictSourceBranch">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Data="{StaticResource Icons.Branch}"/>
<TextBlock Margin="4,0,0,0" Text="{Binding Name}"/>
<TextBlock Margin="4,0,0,0"
Text="{Binding Head, Converter={x:Static c:StringConverters.ToShortSHA}}"
Foreground="DarkOrange"
TextDecorations="Underline"
Cursor="Hand"
PointerPressed="OnPressedSHA"
ToolTip.Tip="{Binding Revision}"
ToolTip.ShowDelay="0">
<TextBlock.DataTemplates>
<DataTemplate DataType="m:Commit">
<StackPanel MinWidth="400" Orientation="Vertical">
<Grid ColumnDefinitions="Auto,*,Auto">
<v:Avatar Grid.Column="0" Width="16" Height="16" VerticalAlignment="Center" IsHitTestVisible="False" User="{Binding Author}"/>
<TextBlock Grid.Column="1" Classes="primary" Text="{Binding Author.Name}" Margin="8,0,0,0"/>
<TextBlock Grid.Column="2" Classes="primary" Text="{Binding CommitterTimeStr}" Foreground="{DynamicResource Brush.FG2}" Margin="8,0,0,0"/>
</Grid>
<TextBlock Classes="primary" Margin="0,8,0,0" Text="{Binding Subject}" TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>
</TextBlock.DataTemplates>
</TextBlock>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="m:Commit">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Data="{StaticResource Icons.Commit}"/>
<v:CommitRefsPresenter Margin="8,0,0,0"
TagBackground="{DynamicResource Brush.DecoratorTag}"
Foreground="{DynamicResource Brush.FG1}"
FontFamily="{DynamicResource Fonts.Primary}"
FontSize="11"
VerticalAlignment="Center"
UseGraphColor="False"/>
<TextBlock Margin="4,0,0,0"
Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}"
Foreground="DarkOrange"
TextDecorations="Underline"
Cursor="Hand"
PointerPressed="OnPressedSHA"
ToolTip.Tip="{Binding}"
ToolTip.ShowDelay="0">
<TextBlock.DataTemplates>
<DataTemplate DataType="m:Commit">
<StackPanel MinWidth="400" Orientation="Vertical">
<Grid ColumnDefinitions="Auto,*,Auto">
<v:Avatar Grid.Column="0" Width="16" Height="16" VerticalAlignment="Center" IsHitTestVisible="False" User="{Binding Author}"/>
<TextBlock Grid.Column="1" Classes="primary" Text="{Binding Author.Name}" Margin="8,0,0,0"/>
<TextBlock Grid.Column="2" Classes="primary" Text="{Binding CommitterTimeStr}" Foreground="{DynamicResource Brush.FG2}" Margin="8,0,0,0"/>
</Grid>
<TextBlock Classes="primary" Margin="0,8,0,0" Text="{Binding Subject}" TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>
</TextBlock.DataTemplates>
</TextBlock>
<TextBlock Margin="4,0,0,0" Text="{Binding Subject}"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="x:String">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Data="{StaticResource Icons.Changes}"/>
<TextBlock Margin="4,0,0,0" Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</StackPanel.DataTemplates>
<Path Width="64" Height="64" Data="{StaticResource Icons.Conflict}" Fill="{DynamicResource Brush.FG2}" HorizontalAlignment="Center"/>
<TextBlock Margin="0,16" FontSize="20" FontWeight="Bold" Text="{DynamicResource Text.WorkingCopy.Conflicts}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Center"/>
<Border Margin="16,0" Padding="8" CornerRadius="4" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}">
<Border.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="Theirs" Converter="{x:Static ObjectConverters.IsNotNull}"/>
<Binding Path="Mine" Converter="{x:Static ObjectConverters.IsNotNull}"/>
</MultiBinding>
</Border.IsVisible>
<Grid Margin="8,0,0,0" RowDefinitions="32,32" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0" Classes="info_label" Text="THEIRS"/>
<ContentControl Grid.Row="0" Grid.Column="1" Margin="16,0,0,0" Content="{Binding Theirs}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Classes="info_label" Text="MINE"/>
<ContentControl Grid.Row="1" Grid.Column="1" Margin="16,0,0,0" Content="{Binding Mine}"/>
</Grid>
</Border>
<StackPanel Margin="0,8,0,0" Orientation="Horizontal" HorizontalAlignment="Center">
<Button Classes="flat" Content="USE THEIRS" Command="{Binding UseTheirs}"/>
<Button Classes="flat" Margin="8,0,0,0" Content="USE MINE" Command="{Binding UseMine}"/>
<Button Classes="flat" Margin="8,0,0,0" Content="OPEN EXTERNAL MERGETOOL" Command="{Binding OpenExternalMergeTool}"/>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Vertical" IsVisible="{Binding IsResolved}">
<Path Width="64" Height="64" Data="{StaticResource Icons.Check}" Fill="Green"/>
<TextBlock Margin="0,16,0,8" FontSize="20" FontWeight="Bold" Text="{DynamicResource Text.WorkingCopy.Conflicts.Resolved}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Center"/>
<TextBlock Text="{DynamicResource Text.WorkingCopy.CanStageTip}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Center"/>
</StackPanel>
</Grid>
</Border>
</UserControl>

View file

@ -0,0 +1,23 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.VisualTree;
namespace SourceGit.Views
{
public partial class Conflict : UserControl
{
public Conflict()
{
InitializeComponent();
}
private void OnPressedSHA(object sender, PointerPressedEventArgs e)
{
var repoView = this.FindAncestorOfType<Repository>();
if (repoView is { DataContext: ViewModels.Repository repo } && sender is TextBlock text)
repo.NavigateToCommit(text.Text);
e.Handled = true;
}
}
}

View file

@ -14,15 +14,7 @@
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.CreateBranch.Title}"/>
<Grid Margin="0,16,0,0" ColumnDefinitions="140,*">
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Margin="0,16,0,0" RowDefinitions="32,32,Auto,Auto,Auto" ColumnDefinitions="140,*">
<TextBlock Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
@ -78,20 +70,12 @@
IsVisible="{Binding !IsBareRepository}"/>
<Border Grid.Row="3" Grid.Column="1" MinHeight="32" IsVisible="{Binding !IsBareRepository}">
<WrapPanel Orientation="Horizontal" VerticalAlignment="Center">
<RadioButton Content="{DynamicResource Text.CreateBranch.LocalChanges.DoNothing}"
x:Name="RadioDoNothing"
GroupName="LocalChanges"
<RadioButton GroupName="LocalChanges"
Margin="0,0,8,0"
IsCheckedChanged="OnLocalChangeActionIsCheckedChanged"/>
<RadioButton Content="{DynamicResource Text.CreateBranch.LocalChanges.StashAndReply}"
x:Name="RadioStashAndReply"
GroupName="LocalChanges"
Margin="0,0,8,0"
IsCheckedChanged="OnLocalChangeActionIsCheckedChanged"/>
<RadioButton Content="{DynamicResource Text.CreateBranch.LocalChanges.Discard}"
x:Name="RadioDiscard"
GroupName="LocalChanges"
IsCheckedChanged="OnLocalChangeActionIsCheckedChanged"/>
Content="{DynamicResource Text.CreateBranch.LocalChanges.StashAndReply}"
IsChecked="{Binding !DiscardLocalChanges, Mode=TwoWay}"/>
<RadioButton GroupName="LocalChanges"
Content="{DynamicResource Text.CreateBranch.LocalChanges.Discard}"/>
</WrapPanel>
</Border>

View file

@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace SourceGit.Views
{
@ -9,51 +8,5 @@ namespace SourceGit.Views
{
InitializeComponent();
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
var vm = DataContext as ViewModels.CreateBranch;
if (vm == null)
return;
switch (vm.PreAction)
{
case Models.DealWithLocalChanges.DoNothing:
RadioDoNothing.IsChecked = true;
break;
case Models.DealWithLocalChanges.StashAndReaply:
RadioStashAndReply.IsChecked = true;
break;
default:
RadioDiscard.IsChecked = true;
break;
}
}
private void OnLocalChangeActionIsCheckedChanged(object sender, RoutedEventArgs e)
{
var vm = DataContext as ViewModels.CreateBranch;
if (vm == null)
return;
if (RadioDoNothing.IsChecked == true)
{
if (vm.PreAction != Models.DealWithLocalChanges.DoNothing)
vm.PreAction = Models.DealWithLocalChanges.DoNothing;
return;
}
if (RadioStashAndReply.IsChecked == true)
{
if (vm.PreAction != Models.DealWithLocalChanges.StashAndReaply)
vm.PreAction = Models.DealWithLocalChanges.StashAndReaply;
return;
}
if (vm.PreAction != Models.DealWithLocalChanges.Discard)
vm.PreAction = Models.DealWithLocalChanges.Discard;
}
}
}

View file

@ -30,7 +30,7 @@
</Border>
<!-- Title -->
<TextBlock Grid.Column="2" Classes="primary" Margin="4,0,0,0" Text="{Binding Title}" FontSize="11" TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Column="2" Classes="primary" Margin="4,0,0,0" Text="{Binding Title}" ToolTip.Tip="{Binding Title}" FontSize="11" TextTrimming="CharacterEllipsis"/>
<!-- Toolbar Buttons -->
<StackPanel Grid.Column="3" Margin="8,0,0,0" Orientation="Horizontal" VerticalAlignment="Center">

View file

@ -10,18 +10,18 @@
x:Class="SourceGit.Views.Histories"
x:DataType="vm:Histories"
x:Name="ThisControl">
<v:LayoutableGrid UseHorizontal="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseTwoColumnsLayoutInHistories}">
<v:LayoutableGrid.RowDefinitions>
<v:HistoriesLayout UseHorizontal="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseTwoColumnsLayoutInHistories}">
<v:HistoriesLayout.RowDefinitions>
<RowDefinition Height="{Binding TopArea, Mode=TwoWay}" MinHeight="100"/>
<RowDefinition Height="3"/>
<RowDefinition Height="{Binding BottomArea, Mode=TwoWay}" MinHeight="200"/>
</v:LayoutableGrid.RowDefinitions>
</v:HistoriesLayout.RowDefinitions>
<v:LayoutableGrid.ColumnDefinitions>
<v:HistoriesLayout.ColumnDefinitions>
<ColumnDefinition Width="{Binding LeftArea, Mode=TwoWay}" MinWidth="100"/>
<ColumnDefinition Width="3"/>
<ColumnDefinition Width="{Binding RightArea, Mode=TwoWay}" MinWidth="100"/>
</v:LayoutableGrid.ColumnDefinitions>
</v:HistoriesLayout.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
<Grid RowDefinitions="24,*" Grid.IsSharedSizeScope="True">
@ -126,7 +126,11 @@
</Grid.ColumnDefinitions>
<!-- Subject & REFS -->
<Border Grid.Column="0" Padding="{Binding Margin}" ClipToBounds="True">
<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">
<v:CommitStatusIndicator Grid.Column="0"
CurrentBranch="{Binding $parent[v:Histories].CurrentBranch}"
@ -159,7 +163,10 @@
</Border>
<!-- Author -->
<Grid Grid.Column="1" ColumnDefinitions="20,*" IsHitTestVisible="False">
<Grid Grid.Column="1"
ColumnDefinitions="20,*"
Background="Transparent"
ToolTip.Tip="{Binding Author}">
<v:Avatar Grid.Column="0"
Width="16" Height="16"
Margin="4,0,0,0"
@ -264,5 +271,5 @@
</ContentControl.DataTemplates>
</ContentControl>
</Grid>
</v:LayoutableGrid>
</v:HistoriesLayout>
</UserControl>

View file

@ -1,24 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace SourceGit.Views
{
public class LayoutableGrid : Grid
public class HistoriesLayout : Grid
{
public static readonly StyledProperty<bool> UseHorizontalProperty =
AvaloniaProperty.Register<LayoutableGrid, bool>(nameof(UseHorizontal));
AvaloniaProperty.Register<HistoriesLayout, bool>(nameof(UseHorizontal));
public bool UseHorizontal
{
@ -28,15 +22,17 @@ namespace SourceGit.Views
protected override Type StyleKeyOverride => typeof(Grid);
static LayoutableGrid()
public HistoriesLayout()
{
UseHorizontalProperty.Changed.AddClassHandler<LayoutableGrid>((o, _) => o.RefreshLayout());
RefreshLayout();
}
public override void ApplyTemplate()
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.ApplyTemplate();
RefreshLayout();
base.OnPropertyChanged(change);
if (change.Property == UseHorizontalProperty)
RefreshLayout();
}
private void RefreshLayout()
@ -74,639 +70,6 @@ namespace SourceGit.Views
}
}
public class CommitStatusIndicator : Control
{
public static readonly StyledProperty<Models.Branch> CurrentBranchProperty =
AvaloniaProperty.Register<CommitStatusIndicator, Models.Branch>(nameof(CurrentBranch));
public Models.Branch CurrentBranch
{
get => GetValue(CurrentBranchProperty);
set => SetValue(CurrentBranchProperty, value);
}
public static readonly StyledProperty<IBrush> AheadBrushProperty =
AvaloniaProperty.Register<CommitStatusIndicator, IBrush>(nameof(AheadBrush));
public IBrush AheadBrush
{
get => GetValue(AheadBrushProperty);
set => SetValue(AheadBrushProperty, value);
}
public static readonly StyledProperty<IBrush> BehindBrushProperty =
AvaloniaProperty.Register<CommitStatusIndicator, IBrush>(nameof(BehindBrush));
public IBrush BehindBrush
{
get => GetValue(BehindBrushProperty);
set => SetValue(BehindBrushProperty, value);
}
enum Status
{
Normal,
Ahead,
Behind,
}
public override void Render(DrawingContext context)
{
if (_status == Status.Normal)
return;
context.DrawEllipse(_status == Status.Ahead ? AheadBrush : BehindBrush, null, new Rect(0, 0, 5, 5));
}
protected override Size MeasureOverride(Size availableSize)
{
if (DataContext is Models.Commit commit && CurrentBranch is not null)
{
var sha = commit.SHA;
var track = CurrentBranch.TrackStatus;
if (track.Ahead.Contains(sha))
_status = Status.Ahead;
else if (track.Behind.Contains(sha))
_status = Status.Behind;
else
_status = Status.Normal;
}
else
{
_status = Status.Normal;
}
return _status == Status.Normal ? new Size(0, 0) : new Size(9, 5);
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
InvalidateMeasure();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == CurrentBranchProperty)
InvalidateMeasure();
}
private Status _status = Status.Normal;
}
public partial class CommitSubjectPresenter : TextBlock
{
public static readonly StyledProperty<string> SubjectProperty =
AvaloniaProperty.Register<CommitSubjectPresenter, string>(nameof(Subject));
public string Subject
{
get => GetValue(SubjectProperty);
set => SetValue(SubjectProperty, value);
}
public static readonly StyledProperty<AvaloniaList<Models.IssueTrackerRule>> IssueTrackerRulesProperty =
AvaloniaProperty.Register<CommitSubjectPresenter, AvaloniaList<Models.IssueTrackerRule>>(nameof(IssueTrackerRules));
public AvaloniaList<Models.IssueTrackerRule> IssueTrackerRules
{
get => GetValue(IssueTrackerRulesProperty);
set => SetValue(IssueTrackerRulesProperty, value);
}
protected override Type StyleKeyOverride => typeof(TextBlock);
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == SubjectProperty || change.Property == IssueTrackerRulesProperty)
{
Inlines!.Clear();
_matches = null;
ClearHoveredIssueLink();
var subject = Subject;
if (string.IsNullOrEmpty(subject))
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;
}
matches.Sort((l, r) => l.Start - r.Start);
_matches = matches;
var inlines = new List<Inline>();
var pos = 0;
foreach (var match in matches)
{
if (match.Start > pos)
{
if (keywordMatch.Success && pos < keywordMatch.Length)
{
if (keywordMatch.Length < match.Start)
{
inlines.Add(new Run(subject.Substring(pos, keywordMatch.Length - pos)) { FontWeight = FontWeight.Bold });
inlines.Add(new Run(subject.Substring(keywordMatch.Length, match.Start - keywordMatch.Length)));
}
else
{
inlines.Add(new Run(subject.Substring(pos, match.Start - pos)) { FontWeight = FontWeight.Bold });
}
}
else
{
inlines.Add(new Run(subject.Substring(pos, match.Start - pos)));
}
}
var link = new Run(subject.Substring(match.Start, match.Length));
link.Classes.Add("issue_link");
inlines.Add(link);
pos = match.Start + match.Length;
}
if (pos < subject.Length)
{
if (keywordMatch.Success && pos < keywordMatch.Length)
{
inlines.Add(new Run(subject.Substring(pos, keywordMatch.Length - pos)) { FontWeight = FontWeight.Bold });
inlines.Add(new Run(subject.Substring(keywordMatch.Length)));
}
else
{
inlines.Add(new Run(subject.Substring(pos)));
}
}
Inlines.AddRange(inlines);
}
}
protected override void OnPointerMoved(PointerEventArgs e)
{
base.OnPointerMoved(e);
if (_matches != null)
{
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))
continue;
if (match == _lastHover)
return;
_lastHover = match;
SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
ToolTip.SetTip(this, match.Link);
ToolTip.SetIsOpen(this, true);
e.Handled = true;
return;
}
ClearHoveredIssueLink();
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if (_lastHover != null)
Native.OS.OpenBrowser(_lastHover.Link);
}
protected override void OnPointerExited(PointerEventArgs e)
{
base.OnPointerExited(e);
ClearHoveredIssueLink();
}
private void ClearHoveredIssueLink()
{
if (_lastHover != null)
{
ToolTip.SetTip(this, null);
SetCurrentValue(CursorProperty, Cursor.Parse("Arrow"));
_lastHover = null;
}
}
[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;
}
public class CommitTimeTextBlock : TextBlock
{
public static readonly StyledProperty<bool> ShowAsDateTimeProperty =
AvaloniaProperty.Register<CommitTimeTextBlock, bool>(nameof(ShowAsDateTime), true);
public bool ShowAsDateTime
{
get => GetValue(ShowAsDateTimeProperty);
set => SetValue(ShowAsDateTimeProperty, value);
}
public static readonly StyledProperty<int> DateTimeFormatProperty =
AvaloniaProperty.Register<CommitTimeTextBlock, int>(nameof(DateTimeFormat), 0);
public int DateTimeFormat
{
get => GetValue(DateTimeFormatProperty);
set => SetValue(DateTimeFormatProperty, value);
}
public static readonly StyledProperty<bool> UseAuthorTimeProperty =
AvaloniaProperty.Register<CommitTimeTextBlock, bool>(nameof(UseAuthorTime), true);
public bool UseAuthorTime
{
get => GetValue(UseAuthorTimeProperty);
set => SetValue(UseAuthorTimeProperty, value);
}
protected override Type StyleKeyOverride => typeof(TextBlock);
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == UseAuthorTimeProperty)
{
SetCurrentValue(TextProperty, GetDisplayText());
}
else if (change.Property == ShowAsDateTimeProperty)
{
SetCurrentValue(TextProperty, GetDisplayText());
if (ShowAsDateTime)
StopTimer();
else
StartTimer();
}
else if (change.Property == DateTimeFormatProperty)
{
if (ShowAsDateTime)
SetCurrentValue(TextProperty, GetDisplayText());
}
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
if (!ShowAsDateTime)
StartTimer();
}
protected override void OnUnloaded(RoutedEventArgs e)
{
base.OnUnloaded(e);
StopTimer();
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
SetCurrentValue(TextProperty, GetDisplayText());
}
private void StartTimer()
{
if (_refreshTimer != null)
return;
_refreshTimer = DispatcherTimer.Run(() =>
{
Dispatcher.UIThread.Invoke(() =>
{
var text = GetDisplayText();
if (!text.Equals(Text, StringComparison.Ordinal))
Text = text;
});
return true;
}, TimeSpan.FromSeconds(10));
}
private void StopTimer()
{
if (_refreshTimer != null)
{
_refreshTimer.Dispose();
_refreshTimer = null;
}
}
private string GetDisplayText()
{
var commit = DataContext as Models.Commit;
if (commit == null)
return string.Empty;
if (ShowAsDateTime)
return UseAuthorTime ? commit.AuthorTimeStr : commit.CommitterTimeStr;
var timestamp = UseAuthorTime ? commit.AuthorTime : commit.CommitterTime;
var now = DateTime.Now;
var localTime = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime();
var span = now - localTime;
if (span.TotalMinutes < 1)
return App.Text("Period.JustNow");
if (span.TotalHours < 1)
return App.Text("Period.MinutesAgo", (int)span.TotalMinutes);
if (span.TotalDays < 1)
return App.Text("Period.HoursAgo", (int)span.TotalHours);
var lastDay = now.AddDays(-1).Date;
if (localTime >= lastDay)
return App.Text("Period.Yesterday");
if ((localTime.Year == now.Year && localTime.Month == now.Month) || span.TotalDays < 28)
{
var diffDay = now.Date - localTime.Date;
return App.Text("Period.DaysAgo", (int)diffDay.TotalDays);
}
var lastMonth = now.AddMonths(-1).Date;
if (localTime.Year == lastMonth.Year && localTime.Month == lastMonth.Month)
return App.Text("Period.LastMonth");
if (localTime.Year == now.Year || localTime > now.AddMonths(-11))
{
var diffMonth = (12 + now.Month - localTime.Month) % 12;
return App.Text("Period.MonthsAgo", diffMonth);
}
var diffYear = now.Year - localTime.Year;
if (diffYear == 1)
return App.Text("Period.LastYear");
return App.Text("Period.YearsAgo", diffYear);
}
private IDisposable _refreshTimer = null;
}
public class CommitGraph : Control
{
public static readonly StyledProperty<Models.CommitGraph> GraphProperty =
AvaloniaProperty.Register<CommitGraph, Models.CommitGraph>(nameof(Graph));
public Models.CommitGraph Graph
{
get => GetValue(GraphProperty);
set => SetValue(GraphProperty, value);
}
public static readonly StyledProperty<IBrush> DotBrushProperty =
AvaloniaProperty.Register<CommitGraph, IBrush>(nameof(DotBrush), Brushes.Transparent);
public IBrush DotBrush
{
get => GetValue(DotBrushProperty);
set => SetValue(DotBrushProperty, value);
}
public static readonly StyledProperty<bool> OnlyHighlightCurrentBranchProperty =
AvaloniaProperty.Register<CommitGraph, bool>(nameof(OnlyHighlightCurrentBranch), true);
public bool OnlyHighlightCurrentBranch
{
get => GetValue(OnlyHighlightCurrentBranchProperty);
set => SetValue(OnlyHighlightCurrentBranchProperty, value);
}
static CommitGraph()
{
AffectsRender<CommitGraph>(GraphProperty, DotBrushProperty, OnlyHighlightCurrentBranchProperty);
}
public override void Render(DrawingContext context)
{
base.Render(context);
var graph = Graph;
if (graph == null)
return;
var histories = this.FindAncestorOfType<Histories>();
if (histories == null)
return;
var list = histories.CommitListContainer;
if (list == null)
return;
// Calculate drawing area.
double width = Bounds.Width - 273 - histories.AuthorNameColumnWidth.Value;
double height = Bounds.Height;
double startY = list.Scroll?.Offset.Y ?? 0;
double endY = startY + height + 28;
// Apply scroll offset and clip.
using (context.PushClip(new Rect(0, 0, width, height)))
using (context.PushTransform(Matrix.CreateTranslation(0, -startY)))
{
// Draw contents
DrawCurves(context, graph, startY, endY);
DrawAnchors(context, graph, startY, endY);
}
}
private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double top, double bottom)
{
var grayedPen = new Pen(new SolidColorBrush(Colors.Gray, 0.4), Models.CommitGraph.Pens[0].Thickness);
var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch;
if (onlyHighlightCurrentBranch)
{
foreach (var link in graph.Links)
{
if (link.IsMerged)
continue;
if (link.End.Y < top)
continue;
if (link.Start.Y > bottom)
break;
var geo = new StreamGeometry();
using (var ctx = geo.Open())
{
ctx.BeginFigure(link.Start, false);
ctx.QuadraticBezierTo(link.Control, link.End);
}
context.DrawGeometry(null, grayedPen, geo);
}
}
foreach (var line in graph.Paths)
{
var last = line.Points[0];
var size = line.Points.Count;
if (line.Points[size - 1].Y < top)
continue;
if (last.Y > bottom)
break;
var geo = new StreamGeometry();
var pen = Models.CommitGraph.Pens[line.Color];
using (var ctx = geo.Open())
{
var started = false;
var ended = false;
for (int i = 1; i < size; i++)
{
var cur = line.Points[i];
if (cur.Y < top)
{
last = cur;
continue;
}
if (!started)
{
ctx.BeginFigure(last, false);
started = true;
}
if (cur.Y > bottom)
{
cur = new Point(cur.X, bottom);
ended = true;
}
if (cur.X > last.X)
{
ctx.QuadraticBezierTo(new Point(cur.X, last.Y), cur);
}
else if (cur.X < last.X)
{
if (i < size - 1)
{
var midY = (last.Y + cur.Y) / 2;
ctx.CubicBezierTo(new Point(last.X, midY + 4), new Point(cur.X, midY - 4), cur);
}
else
{
ctx.QuadraticBezierTo(new Point(last.X, cur.Y), cur);
}
}
else
{
ctx.LineTo(cur);
}
if (ended)
break;
last = cur;
}
}
if (!line.IsMerged && onlyHighlightCurrentBranch)
context.DrawGeometry(null, grayedPen, geo);
else
context.DrawGeometry(null, pen, geo);
}
foreach (var link in graph.Links)
{
if (onlyHighlightCurrentBranch && !link.IsMerged)
continue;
if (link.End.Y < top)
continue;
if (link.Start.Y > bottom)
break;
var geo = new StreamGeometry();
using (var ctx = geo.Open())
{
ctx.BeginFigure(link.Start, false);
ctx.QuadraticBezierTo(link.Control, link.End);
}
context.DrawGeometry(null, Models.CommitGraph.Pens[link.Color], geo);
}
}
private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, double top, double bottom)
{
var dotFill = DotBrush;
var dotFillPen = new Pen(dotFill, 2);
var grayedPen = new Pen(Brushes.Gray, Models.CommitGraph.Pens[0].Thickness);
var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch;
foreach (var dot in graph.Dots)
{
if (dot.Center.Y < top)
continue;
if (dot.Center.Y > bottom)
break;
var pen = Models.CommitGraph.Pens[dot.Color];
if (!dot.IsMerged && onlyHighlightCurrentBranch)
pen = grayedPen;
switch (dot.Type)
{
case Models.CommitGraph.DotType.Head:
context.DrawEllipse(dotFill, pen, dot.Center, 6, 6);
context.DrawEllipse(pen.Brush, null, dot.Center, 3, 3);
break;
case Models.CommitGraph.DotType.Merge:
context.DrawEllipse(pen.Brush, null, dot.Center, 6, 6);
context.DrawLine(dotFillPen, new Point(dot.Center.X, dot.Center.Y - 3), new Point(dot.Center.X, dot.Center.Y + 3));
context.DrawLine(dotFillPen, new Point(dot.Center.X - 3, dot.Center.Y), new Point(dot.Center.X + 3, dot.Center.Y));
break;
default:
context.DrawEllipse(dotFill, pen, dot.Center, 3, 3);
break;
}
}
}
}
public partial class Histories : UserControl
{
public static readonly StyledProperty<GridLength> AuthorNameColumnWidthProperty =
@ -754,36 +117,34 @@ namespace SourceGit.Views
set => SetValue(NavigationIdProperty, value);
}
static Histories()
{
NavigationIdProperty.Changed.AddClassHandler<Histories>((h, _) =>
{
if (h.DataContext == null)
return;
// Force scroll selected item (current head) into view. see issue #58
var list = h.CommitListContainer;
if (list != null && list.SelectedItems.Count == 1)
list.ScrollIntoView(list.SelectedIndex);
});
AuthorNameColumnWidthProperty.Changed.AddClassHandler<Histories>((h, _) =>
{
h.CommitGraph.InvalidateVisual();
});
}
public Histories()
{
InitializeComponent();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == NavigationIdProperty)
{
if (DataContext is ViewModels.Histories)
{
var list = CommitListContainer;
if (list != null && list.SelectedItems.Count == 1)
list.ScrollIntoView(list.SelectedIndex);
}
}
}
private void OnCommitListLayoutUpdated(object _1, EventArgs _2)
{
var y = CommitListContainer.Scroll?.Offset.Y ?? 0;
if (y != _lastScrollY)
var authorNameColumnWidth = AuthorNameColumnWidth.Value;
if (y != _lastScrollY || authorNameColumnWidth != _lastAuthorNameColumnWidth)
{
_lastScrollY = y;
_lastAuthorNameColumnWidth = authorNameColumnWidth;
CommitGraph.InvalidateVisual();
}
}
@ -863,5 +224,6 @@ namespace SourceGit.Views
}
private double _lastScrollY = 0;
private double _lastAuthorNameColumnWidth = 0;
}
}

View file

@ -273,8 +273,10 @@ namespace SourceGit.Views
protected override void OnClosing(WindowClosingEventArgs e)
{
(DataContext as ViewModels.Launcher)?.Quit(Width, Height);
base.OnClosing(e);
if (!Design.IsDesignMode && DataContext is ViewModels.Launcher launcher)
launcher.Quit(Width, Height);
}
private void OnOpenWorkspaceMenu(object sender, RoutedEventArgs e)

View file

@ -422,7 +422,7 @@
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preferences.Integration}"/>
</TabItem.Header>
<StackPanel Margin="8" MaxWidth="580" Orientation="Vertical" Grid.IsSharedSizeScope="True">
<StackPanel Margin="8" Orientation="Vertical" Grid.IsSharedSizeScope="True">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Data="{StaticResource Icons.Terminal}"/>
<TextBlock Classes="bold" Margin="4,0,0,0" Text="{DynamicResource Text.Preferences.Shell}"/>
@ -527,12 +527,126 @@
</StackPanel>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Configure.CustomAction}"/>
</TabItem.Header>
<Grid MinHeight="340" Margin="0,8,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*" MaxWidth="400"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}"
Background="{DynamicResource Brush.Contents}">
<Grid RowDefinitions="*,1,Auto">
<ListBox Grid.Row="0"
Background="Transparent"
ItemsSource="{Binding CustomActions}"
SelectedItem="{Binding #ThisControl.SelectedCustomAction, Mode=TwoWay}"
SelectionMode="Single">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="MinHeight" Value="0"/>
<Setter Property="Height" Value="26"/>
<Setter Property="Padding" Value="4,2"/>
</Style>
</ListBox.Styles>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="m:CustomAction">
<Grid Margin="4,0" ColumnDefinitions="Auto,*">
<Path Grid.Column="0" Width="12" Height="12" Data="{StaticResource Icons.Action}" Fill="{DynamicResource Brush.FG1}"/>
<TextBlock Grid.Column="1" Text="{Binding Name}" Margin="6,0,0,0" TextTrimming="CharacterEllipsis"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Rectangle Grid.Row="1" Height="1" Fill="{DynamicResource Brush.Border2}" HorizontalAlignment="Stretch" VerticalAlignment="Bottom"/>
<StackPanel Grid.Row="2" Orientation="Horizontal" Background="{DynamicResource Brush.ToolBar}">
<Button Classes="icon_button" Click="OnAddCustomAction">
<Path Width="14" Height="14" Data="{StaticResource Icons.Plus}"/>
</Button>
<Rectangle Width="1" Fill="{DynamicResource Brush.Border2}" HorizontalAlignment="Left" VerticalAlignment="Stretch"/>
<Button Classes="icon_button" Click="OnRemoveSelectedCustomAction">
<Path Width="14" Height="14" Data="{StaticResource Icons.Window.Minimize}"/>
</Button>
<Rectangle Width="1" Fill="{DynamicResource Brush.Border2}" HorizontalAlignment="Left" VerticalAlignment="Stretch"/>
</StackPanel>
</Grid>
</Border>
<ContentControl Grid.Column="1" Margin="16,0,0,0">
<ContentControl.Content>
<Binding Path="SelectedCustomAction" ElementName="ThisControl">
<Binding.TargetNullValue>
<Path Width="64" Height="64"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Fill="{DynamicResource Brush.FG2}"
Data="{StaticResource Icons.Empty}"/>
</Binding.TargetNullValue>
</Binding>
</ContentControl.Content>
<ContentControl.DataTemplates>
<DataTemplate DataType="m:CustomAction">
<StackPanel Orientation="Vertical">
<TextBlock Text="{DynamicResource Text.Configure.CustomAction.Name}"/>
<TextBox Margin="0,4,0,0" CornerRadius="3" Height="28" Text="{Binding Name, Mode=TwoWay}"/>
<TextBlock Margin="0,12,0,0" Text="{DynamicResource Text.Configure.CustomAction.Scope}"/>
<ComboBox Margin="0,4,0,0" Height="28" HorizontalAlignment="Stretch" SelectedIndex="{Binding Scope, Mode=TwoWay}">
<ComboBoxItem Content="{DynamicResource Text.Configure.CustomAction.Scope.Repository}"/>
<ComboBoxItem Content="{DynamicResource Text.Configure.CustomAction.Scope.Commit}"/>
<ComboBoxItem Content="{DynamicResource Text.Configure.CustomAction.Scope.Branch}"/>
</ComboBox>
<TextBlock Margin="0,12,0,0" Text="{DynamicResource Text.Configure.CustomAction.Executable}"/>
<TextBox Margin="0,4,0,0" Height="28" CornerRadius="3" Text="{Binding Executable, Mode=TwoWay}">
<TextBox.InnerRightContent>
<Button Classes="icon_button" Width="30" Height="30" Click="SelectExecutableForCustomAction">
<Path Data="{StaticResource Icons.Folder.Open}" Fill="{DynamicResource Brush.FG1}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
<TextBlock Margin="0,12,0,0" Text="{DynamicResource Text.Configure.CustomAction.Arguments}"/>
<TextBox Margin="0,4,0,0" CornerRadius="3" Height="28" Text="{Binding Arguments, Mode=TwoWay}"/>
<TextBlock Margin="0,2,0,0" TextWrapping="Wrap" Text="{DynamicResource Text.Configure.CustomAction.Arguments.Tip}" Foreground="{DynamicResource Brush.FG2}"/>
<CheckBox Margin="0,8,0,0" Content="{DynamicResource Text.Configure.CustomAction.WaitForExit}" IsChecked="{Binding WaitForExit, Mode=TwoWay}"/>
</StackPanel>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</Grid>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preferences.AI}"/>
</TabItem.Header>
<Grid ColumnDefinitions="200,*" Margin="0,8,0,16" MinHeight="400">
<Grid Margin="0,8,0,16" MinHeight="400">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*" MaxWidth="400"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}"
Background="{DynamicResource Brush.Contents}">

View file

@ -103,6 +103,15 @@ namespace SourceGit.Views
set => SetValue(SelectedOpenAIServiceProperty, value);
}
public static readonly StyledProperty<Models.CustomAction> SelectedCustomActionProperty =
AvaloniaProperty.Register<Preferences, Models.CustomAction>(nameof(SelectedCustomAction));
public Models.CustomAction SelectedCustomAction
{
get => GetValue(SelectedCustomActionProperty);
set => SetValue(SelectedCustomActionProperty, value);
}
public Preferences()
{
var pref = ViewModels.Preferences.Instance;
@ -160,6 +169,11 @@ namespace SourceGit.Views
protected override void OnClosing(WindowClosingEventArgs e)
{
base.OnClosing(e);
if (Design.IsDesignMode)
return;
var config = new Commands.Config(null).ListAll();
SetIfChanged(config, "user.name", DefaultUser, "");
SetIfChanged(config, "user.email", DefaultEmail, "");
@ -190,7 +204,6 @@ namespace SourceGit.Views
}
ViewModels.Preferences.Instance.Save();
base.OnClosing(e);
}
private async void SelectThemeOverrideFile(object _, RoutedEventArgs e)
@ -368,6 +381,40 @@ namespace SourceGit.Views
e.Handled = true;
}
private void OnAddCustomAction(object sender, RoutedEventArgs e)
{
var action = new Models.CustomAction() { Name = "Unnamed Action (Global)" };
ViewModels.Preferences.Instance.CustomActions.Add(action);
SelectedCustomAction = action;
e.Handled = true;
}
private async void SelectExecutableForCustomAction(object sender, RoutedEventArgs e)
{
var options = new FilePickerOpenOptions()
{
FileTypeFilter = [new FilePickerFileType("Executable file(script)") { Patterns = ["*.*"] }],
AllowMultiple = false,
};
var selected = await StorageProvider.OpenFilePickerAsync(options);
if (selected.Count == 1 && sender is Button { DataContext: Models.CustomAction action })
action.Executable = selected[0].Path.LocalPath;
e.Handled = true;
}
private void OnRemoveSelectedCustomAction(object sender, RoutedEventArgs e)
{
if (SelectedCustomAction == null)
return;
ViewModels.Preferences.Instance.CustomActions.Remove(SelectedCustomAction);
SelectedCustomAction = null;
e.Handled = true;
}
private void UpdateGitVersion()
{
GitVersion = Native.OS.GitVersionString;

View file

@ -84,20 +84,12 @@
Margin="0,0,8,0"
Text="{DynamicResource Text.Pull.LocalChanges}"/>
<WrapPanel Grid.Row="3" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<RadioButton Content="{DynamicResource Text.Pull.LocalChanges.DoNothing}"
x:Name="RadioDoNothing"
<RadioButton GroupName="LocalChanges"
Margin="0,0,8,0"
GroupName="LocalChanges"
IsCheckedChanged="OnLocalChangeActionIsCheckedChanged"/>
<RadioButton Content="{DynamicResource Text.Pull.LocalChanges.StashAndReply}"
x:Name="RadioStashAndReply"
Margin="0,0,8,0"
GroupName="LocalChanges"
IsCheckedChanged="OnLocalChangeActionIsCheckedChanged"/>
<RadioButton Content="{DynamicResource Text.Pull.LocalChanges.Discard}"
x:Name="RadioDiscard"
GroupName="LocalChanges"
IsCheckedChanged="OnLocalChangeActionIsCheckedChanged"/>
Content="{DynamicResource Text.Pull.LocalChanges.StashAndReply}"
IsChecked="{Binding !DiscardLocalChanges, Mode=TwoWay}"/>
<RadioButton GroupName="LocalChanges"
Content="{DynamicResource Text.Pull.LocalChanges.Discard}"/>
</WrapPanel>
<CheckBox Grid.Row="4" Grid.Column="1"

View file

@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace SourceGit.Views
{
@ -9,51 +8,5 @@ namespace SourceGit.Views
{
InitializeComponent();
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
var vm = DataContext as ViewModels.Pull;
if (vm == null)
return;
switch (vm.PreAction)
{
case Models.DealWithLocalChanges.DoNothing:
RadioDoNothing.IsChecked = true;
break;
case Models.DealWithLocalChanges.StashAndReaply:
RadioStashAndReply.IsChecked = true;
break;
default:
RadioDiscard.IsChecked = true;
break;
}
}
private void OnLocalChangeActionIsCheckedChanged(object sender, RoutedEventArgs e)
{
var vm = DataContext as ViewModels.Pull;
if (vm == null)
return;
if (RadioDoNothing.IsChecked == true)
{
if (vm.PreAction != Models.DealWithLocalChanges.DoNothing)
vm.PreAction = Models.DealWithLocalChanges.DoNothing;
return;
}
if (RadioStashAndReply.IsChecked == true)
{
if (vm.PreAction != Models.DealWithLocalChanges.StashAndReaply)
vm.PreAction = Models.DealWithLocalChanges.StashAndReaply;
return;
}
if (vm.PreAction != Models.DealWithLocalChanges.Discard)
vm.PreAction = Models.DealWithLocalChanges.Discard;
}
}
}

View file

@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.RenameBranch"
x:DataType="vm:RenameBranch">
@ -11,7 +12,7 @@
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.RenameBranch}"/>
<Grid Margin="0,16,0,0" RowDefinitions="32,32" ColumnDefinitions="120,*">
<Grid Margin="0,16,0,0" RowDefinitions="32,32,Auto" ColumnDefinitions="120,*">
<TextBlock Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
@ -32,6 +33,13 @@
Text="{Binding Name, Mode=TwoWay}"
Watermark="{DynamicResource Text.RenameBranch.Name.Placeholder}"
v:AutoFocusBehaviour.IsEnabled="True"/>
<StackPanel Grid.Row="2" Grid.Column="1"
Orientation="Horizontal"
IsVisible="{Binding Name, Converter={x:Static c:StringConverters.ContainsSpaces}}">
<Path Width="10" Height="10" Data="{StaticResource Icons.Error}" Fill="DarkOrange"/>
<TextBlock Classes="small" Text="{DynamicResource Text.CreateBranch.Name.WarnSpace}" Margin="4,0,0,0"/>
</StackPanel>
</Grid>
</StackPanel>
</UserControl>

View file

@ -421,14 +421,20 @@
<Popup PlacementTarget="{Binding #TxtSearchCommitsBox}"
Placement="BottomEdgeAlignedLeft"
HorizontalOffset="-8" VerticalAlignment="-8"
IsOpen="{Binding IsSearchCommitSuggestionOpen}">
HorizontalOffset="-8" VerticalAlignment="-8">
<Popup.IsOpen>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="MatchedFilesForSearching" Converter="{x:Static c:ListConverters.IsNotNullOrEmpty}"/>
<Binding Path="$parent[Window].IsActive"/>
</MultiBinding>
</Popup.IsOpen>
<Border Margin="8" VerticalAlignment="Top" Effect="drop-shadow(0 0 8 #80000000)">
<Border Background="{DynamicResource Brush.Popup}" CornerRadius="4" Padding="4" BorderThickness="0.65" BorderBrush="{DynamicResource Brush.Accent}">
<ListBox x:Name="SearchSuggestionBox"
Background="Transparent"
SelectionMode="Single"
ItemsSource="{Binding SearchCommitFilterSuggestion}"
ItemsSource="{Binding MatchedFilesForSearching}"
MaxHeight="400"
Focusable="True"
KeyDown="OnSearchSuggestionBoxKeyDown">
@ -478,11 +484,11 @@
BorderThickness="0"
SelectedIndex="{Binding SearchCommitFilterType, Mode=TwoWay}">
<ComboBox.Items>
<TextBlock Text="{DynamicResource Text.Repository.Search.BySHA}" FontSize="12"/>
<TextBlock Text="{DynamicResource Text.Repository.Search.ByAuthor}" FontSize="12"/>
<TextBlock Text="{DynamicResource Text.Repository.Search.ByCommitter}" FontSize="12"/>
<TextBlock Text="{DynamicResource Text.Repository.Search.ByMessage}" FontSize="12"/>
<TextBlock Text="{DynamicResource Text.Repository.Search.ByFile}" FontSize="12"/>
<TextBlock Text="{DynamicResource Text.Repository.Search.BySHA}"/>
<TextBlock Text="{DynamicResource Text.Repository.Search.ByAuthor}"/>
<TextBlock Text="{DynamicResource Text.Repository.Search.ByCommitter}"/>
<TextBlock Text="{DynamicResource Text.Repository.Search.ByMessage}"/>
<TextBlock Text="{DynamicResource Text.Repository.Search.ByFile}"/>
</ComboBox.Items>
</ComboBox>
@ -490,26 +496,29 @@
Margin="4,0,0,0"
IsChecked="{Binding OnlySearchCommitsInCurrentBranch, Mode=TwoWay}"
IsVisible="{Binding SearchCommitFilterType, Converter={x:Static c:IntConverters.IsGreaterThanZero}}">
<TextBlock Text="{DynamicResource Text.Repository.Search.InCurrentBranch}" FontSize="12"/>
<TextBlock Text="{DynamicResource Text.Repository.Search.InCurrentBranch}"/>
</CheckBox>
</StackPanel>
<ListBox Grid.Row="2"
Margin="0,8,0,0"
Padding="4"
ItemsSource="{Binding SearchedCommits}"
SelectionMode="Single"
SelectedItem="{Binding SearchResultSelectedCommit, Mode=TwoWay}"
SelectedItem="{Binding SelectedSearchedCommit, Mode=TwoWay}"
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border2}"
Background="{DynamicResource Brush.Contents}"
CornerRadius="4"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto">
ScrollViewer.VerticalScrollBarVisibility="Auto"
Grid.IsSharedSizeScope="True">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Margin" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Height" Value="50"/>
<Setter Property="Height" Value="28"/>
<Setter Property="CornerRadius" Value="4"/>
</Style>
</ListBox.Styles>
@ -521,18 +530,22 @@
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Commit">
<Border BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="0,0,0,1" Padding="4" Background="Transparent">
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto">
<v:Avatar Grid.Column="0" Width="16" Height="16" VerticalAlignment="Center" IsHitTestVisible="False" User="{Binding Author}"/>
<TextBlock Grid.Column="1" Classes="primary" Text="{Binding Author.Name}" Margin="8,0,0,0" ClipToBounds="True"/>
<TextBlock Grid.Column="2" Classes="primary" Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" Foreground="DarkOrange" Margin="8,0,0,0"/>
<TextBlock Grid.Column="3" Classes="primary" Text="{Binding AuthorTimeShortStr}" Foreground="{DynamicResource Brush.FG2}" Margin="8,0,0,0"/>
</Grid>
<TextBlock Grid.Row="1" Classes="primary" Text="{Binding Subject}" VerticalAlignment="Bottom"/>
</Grid>
</Border>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="SearchCommitTimeColumn"/>
</Grid.ColumnDefinitions>
<v:Avatar Grid.Column="0" Width="16" Height="16" HorizontalAlignment="Center" VerticalAlignment="Center" IsHitTestVisible="False" User="{Binding Author}"/>
<TextBlock Grid.Column="1" Classes="primary" Text="{Binding Subject}" Margin="2,0,0,0" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Column="2"
Classes="primary"
Margin="4,0"
Foreground="{DynamicResource Brush.FG2}"
Text="{Binding CommitterTimeShortStr}"
HorizontalAlignment="Right" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

View file

@ -134,7 +134,7 @@ namespace SourceGit.Views
}
else if (e.Key == Key.Down)
{
if (repo.IsSearchCommitSuggestionOpen)
if (repo.MatchedFilesForSearching is { Count: > 0 })
{
SearchSuggestionBox.Focus(NavigationMethod.Tab);
SearchSuggestionBox.SelectedIndex = 0;
@ -144,12 +144,7 @@ namespace SourceGit.Views
}
else if (e.Key == Key.Escape)
{
if (repo.IsSearchCommitSuggestionOpen)
{
repo.SearchCommitFilterSuggestion.Clear();
repo.IsSearchCommitSuggestionOpen = false;
}
repo.ClearMatchedFilesForSearching();
e.Handled = true;
}
}
@ -369,9 +364,7 @@ namespace SourceGit.Views
if (e.Key == Key.Escape)
{
repo.IsSearchCommitSuggestionOpen = false;
repo.SearchCommitFilterSuggestion.Clear();
repo.ClearMatchedFilesForSearching();
e.Handled = true;
}
else if (e.Key == Key.Enter && SearchSuggestionBox.SelectedItem is string content)

View file

@ -13,8 +13,10 @@ namespace SourceGit.Views
protected override void OnClosing(WindowClosingEventArgs e)
{
(DataContext as ViewModels.RepositoryConfigure)?.Save();
base.OnClosing(e);
if (!Design.IsDesignMode && DataContext is ViewModels.RepositoryConfigure configure)
configure.Save();
}
private async void SelectExecutableForCustomAction(object sender, RoutedEventArgs e)

View file

@ -43,8 +43,14 @@
<Popup PlacementTarget="{Binding #TxtSearchRevisionFiles}"
Placement="BottomEdgeAlignedLeft"
HorizontalOffset="-8" VerticalAlignment="-8"
IsOpen="{Binding RevisionFileSearchSuggestion, Converter={x:Static c:ListConverters.IsNotNullOrEmpty}}">
HorizontalOffset="-8" VerticalAlignment="-8">
<Popup.IsOpen>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="RevisionFileSearchSuggestion" Converter="{x:Static c:ListConverters.IsNotNullOrEmpty}"/>
<Binding Path="$parent[Window].IsActive"/>
</MultiBinding>
</Popup.IsOpen>
<Border Margin="8" VerticalAlignment="Top" Effect="drop-shadow(0 0 8 #80000000)">
<Border Background="{DynamicResource Brush.Popup}" CornerRadius="4" Padding="4" BorderThickness="0.65" BorderBrush="{DynamicResource Brush.Accent}">
<ListBox x:Name="SearchSuggestionBox"

View file

@ -203,84 +203,7 @@
<ContentControl Content="{Binding DetailContext}">
<ContentControl.DataTemplates>
<DataTemplate DataType="vm:Conflict">
<Border Background="{DynamicResource Brush.Window}" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}">
<Grid VerticalAlignment="Center">
<StackPanel Orientation="Vertical" IsVisible="{Binding !IsResolved}">
<StackPanel.DataTemplates>
<DataTemplate DataType="m:Branch">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Data="{StaticResource Icons.Branch}"/>
<TextBlock Margin="4,0,0,0" Text="{Binding FriendlyName}"/>
<TextBlock Margin="4,0,0,0"
Text="{Binding Head, Converter={x:Static c:StringConverters.ToShortSHA}}"
Foreground="DarkOrange"
TextDecorations="Underline"
Cursor="Hand"
PointerPressed="OnPressedSHA"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="m:Commit">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Data="{StaticResource Icons.Commit}"/>
<v:CommitRefsPresenter Margin="8,0,0,0"
TagBackground="{DynamicResource Brush.DecoratorTag}"
Foreground="{DynamicResource Brush.FG1}"
FontFamily="{DynamicResource Fonts.Primary}"
FontSize="11"
VerticalAlignment="Center"
UseGraphColor="False"/>
<TextBlock Margin="4,0,0,0"
Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}"
Foreground="DarkOrange"
TextDecorations="Underline"
Cursor="Hand"
PointerPressed="OnPressedSHA"/>
<TextBlock Margin="4,0,0,0" Text="{Binding Subject}"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="x:String">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Data="{StaticResource Icons.Changes}"/>
<TextBlock Margin="4,0,0,0" Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</StackPanel.DataTemplates>
<Path Width="64" Height="64" Data="{StaticResource Icons.Conflict}" Fill="{DynamicResource Brush.FG2}" HorizontalAlignment="Center"/>
<TextBlock Margin="0,16" FontSize="20" FontWeight="Bold" Text="{DynamicResource Text.WorkingCopy.Conflicts}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Center"/>
<Border Margin="16,0" Padding="8" CornerRadius="4" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}">
<Border.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="Theirs" Converter="{x:Static ObjectConverters.IsNotNull}"/>
<Binding Path="Mine" Converter="{x:Static ObjectConverters.IsNotNull}"/>
</MultiBinding>
</Border.IsVisible>
<Grid Margin="8,0,0,0" RowDefinitions="32,32" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0" Classes="info_label" Text="THEIRS"/>
<ContentControl Grid.Row="0" Grid.Column="1" Margin="16,0,0,0" Content="{Binding Theirs}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Classes="info_label" Text="MINE"/>
<ContentControl Grid.Row="1" Grid.Column="1" Margin="16,0,0,0" Content="{Binding Mine}"/>
</Grid>
</Border>
<StackPanel Margin="0,8,0,0" Orientation="Horizontal" HorizontalAlignment="Center">
<Button Classes="flat" Content="USE THEIRS" Command="{Binding UseTheirs}"/>
<Button Classes="flat" Margin="8,0,0,0" Content="USE MINE" Command="{Binding UseMine}"/>
<Button Classes="flat" Margin="8,0,0,0" Content="OPEN EXTERNAL MERGETOOL" Command="{Binding OpenExternalMergeTool}"/>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Vertical" IsVisible="{Binding IsResolved}">
<Path Width="64" Height="64" Data="{StaticResource Icons.Check}" Fill="Green"/>
<TextBlock Margin="0,16,0,8" FontSize="20" FontWeight="Bold" Text="{DynamicResource Text.WorkingCopy.Conflicts.Resolved}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Center"/>
<TextBlock Text="{DynamicResource Text.WorkingCopy.CanStageTip}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Center"/>
</StackPanel>
</Grid>
</Border>
<v:Conflict/>
</DataTemplate>
<DataTemplate DataType="vm:DiffContext">

View file

@ -1,7 +1,6 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace SourceGit.Views
{
@ -160,14 +159,5 @@ namespace SourceGit.Views
e.Handled = true;
}
private void OnPressedSHA(object sender, PointerPressedEventArgs e)
{
var repoView = this.FindAncestorOfType<Repository>();
if (repoView is { DataContext: ViewModels.Repository repo } && sender is TextBlock text)
repo.NavigateToCommit(text.Text);
e.Handled = true;
}
}
}