Merge branch 'release/v8.16'

This commit is contained in:
leo 2024-06-11 09:16:06 +08:00
commit e7c52d0eaa
No known key found for this signature in database
GPG key ID: B528468E49CD0E58
81 changed files with 1875 additions and 1285 deletions

View file

@ -7,7 +7,7 @@ Opensource Git GUI client.
* Supports Windows/macOS/Linux
* Opensource/Free
* Fast
* English/简体中文
* English/简体中文/繁體中文
* Built-in light/dark themes
* Visual commit graph
* Supports SSH access with each remote
@ -75,6 +75,7 @@ This app supports open repository in external tools listed in the table below.
> * You can set the given environment variable for special tool if it can NOT be found by this app automatically.
> * Installing `JetBrains Toolbox` will help this app to find other JetBrains tools installed on your device.
> * On macOS, you may need to use `launchctl setenv` to make sure the app can read these environment variables.
## Screenshots

View file

@ -1 +1 @@
8.15
8.16

View file

@ -12,6 +12,11 @@
<string>SOURCE_GIT_VERSION.0</string>
<key>LSMinimumSystemVersion</key>
<string>10.12</string>
<key>LSEnvironment</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
<key>CFBundleExecutable</key>
<string>SourceGit</string>
<key>CFBundleInfoDictionaryVersion</key>

View file

@ -1,10 +1,12 @@
using System.Text.Json.Serialization;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace SourceGit
{
[JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true)]
[JsonSerializable(typeof(Models.Version))]
[JsonSerializable(typeof(Models.JetBrainsState))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(ViewModels.Preference))]
internal partial class JsonCodeGen : JsonSerializerContext { }
}

View file

@ -13,6 +13,7 @@
<ResourceInclude x:Key="en_US" Source="/Resources/Locales/en_US.axaml"/>
<ResourceInclude x:Key="zh_CN" Source="/Resources/Locales/zh_CN.axaml"/>
<ResourceInclude x:Key="zh_TW" Source="/Resources/Locales/zh_TW.axaml"/>
</ResourceDictionary>
</Application.Resources>

View file

@ -132,32 +132,48 @@ namespace SourceGit
var app = Current as App;
var targetLocale = app.Resources[localeKey] as ResourceDictionary;
if (targetLocale == null || targetLocale == app._activeLocale)
{
return;
}
if (app._activeLocale != null)
{
app.Resources.MergedDictionaries.Remove(app._activeLocale);
}
app.Resources.MergedDictionaries.Add(targetLocale);
app._activeLocale = targetLocale;
}
public static void SetTheme(string theme)
public static void SetTheme(string theme, string colorsFile)
{
var app = Current as App;
if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase))
{
Current.RequestedThemeVariant = ThemeVariant.Light;
}
app.RequestedThemeVariant = ThemeVariant.Light;
else if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase))
{
Current.RequestedThemeVariant = ThemeVariant.Dark;
}
app.RequestedThemeVariant = ThemeVariant.Dark;
else
app.RequestedThemeVariant = ThemeVariant.Default;
if (app._colorOverrides != null)
{
Current.RequestedThemeVariant = ThemeVariant.Default;
app.Resources.MergedDictionaries.Remove(app._colorOverrides);
app._colorOverrides = null;
}
if (!string.IsNullOrEmpty(colorsFile) && File.Exists(colorsFile))
{
try
{
var resDic = new ResourceDictionary();
var schema = JsonSerializer.Deserialize(File.ReadAllText(colorsFile), JsonCodeGen.Default.DictionaryStringString);
foreach (var kv in schema)
resDic[kv.Key] = Color.Parse(kv.Value);
app.Resources.MergedDictionaries.Add(resDic);
app._colorOverrides = resDic;
}
catch
{
}
}
}
@ -166,9 +182,7 @@ namespace SourceGit
if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
if (desktop.MainWindow.Clipboard is { } clipbord)
{
await clipbord.SetTextAsync(data);
}
}
}
@ -256,7 +270,7 @@ namespace SourceGit
var pref = ViewModels.Preference.Instance;
SetLocale(pref.Locale);
SetTheme(pref.Theme);
SetTheme(pref.Theme, pref.ColorOverrides);
}
public override void OnFrameworkInitializationCompleted()
@ -300,6 +314,7 @@ namespace SourceGit
}
private ResourceDictionary _activeLocale = null;
private ResourceDictionary _colorOverrides = null;
private Models.INotificationReceiver _notificationReceiver = null;
}
}

View file

@ -0,0 +1,19 @@
namespace SourceGit.Commands
{
public class QueryCommitFullMessage : Command
{
public QueryCommitFullMessage(string repo, string sha)
{
WorkingDirectory = repo;
Context = repo;
Args = $"show --no-show-signature --pretty=format:%B -s {sha}";
}
public string Result()
{
var rs = ReadToEnd();
if (rs.IsSuccess) return rs.StdOut.TrimEnd();
return string.Empty;
}
}
}

View file

@ -5,133 +5,101 @@ namespace SourceGit.Commands
{
public class QueryCommits : Command
{
private const string GPGSIG_START = "gpgsig -----BEGIN ";
private const string GPGSIG_END = " -----END ";
private readonly List<Models.Commit> commits = new List<Models.Commit>();
private Models.Commit current = null;
private bool isSkipingGpgsig = false;
private bool isHeadFounded = false;
private readonly bool findFirstMerged = true;
public QueryCommits(string repo, string limits, bool needFindHead = true)
{
WorkingDirectory = repo;
Context = repo;
Args = "log --date-order --decorate=full --pretty=raw " + limits;
findFirstMerged = needFindHead;
Args = $"log --date-order --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + limits;
_findFirstMerged = needFindHead;
}
public List<Models.Commit> Result()
{
Exec();
var rs = ReadToEnd();
if (!rs.IsSuccess)
return _commits;
if (current != null)
var nextPartIdx = 0;
var start = 0;
var end = rs.StdOut.IndexOf('\n', start);
while (end > 0)
{
current.Message = current.Message.Trim();
commits.Add(current);
var line = rs.StdOut.Substring(start, end - start);
switch (nextPartIdx)
{
case 0:
_current = new Models.Commit() { SHA = line };
_commits.Add(_current);
break;
case 1:
ParseParent(line);
break;
case 2:
ParseDecorators(line);
break;
case 3:
_current.Author = Models.User.FindOrAdd(line);
break;
case 4:
_current.AuthorTime = ulong.Parse(line);
break;
case 5:
_current.Committer = Models.User.FindOrAdd(line);
break;
case 6:
_current.CommitterTime = ulong.Parse(line);
break;
case 7:
_current.Subject = line;
nextPartIdx = -1;
break;
default:
break;
}
nextPartIdx++;
start = end + 1;
end = rs.StdOut.IndexOf('\n', start);
}
if (findFirstMerged && !isHeadFounded && commits.Count > 0)
{
if (_findFirstMerged && !_isHeadFounded && _commits.Count > 0)
MarkFirstMerged();
}
return commits;
return _commits;
}
protected override void OnReadline(string line)
private void ParseParent(string data)
{
if (isSkipingGpgsig)
{
if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal))
isSkipingGpgsig = false;
if (data.Length < 8)
return;
}
else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal))
var idx = data.IndexOf(' ', StringComparison.Ordinal);
if (idx == -1)
{
isSkipingGpgsig = true;
_current.Parents.Add(data);
return;
}
if (line.StartsWith("commit ", StringComparison.Ordinal))
{
if (current != null)
{
current.Message = current.Message.Trim();
commits.Add(current);
}
current = new Models.Commit();
line = line.Substring(7);
var decoratorStart = line.IndexOf('(', StringComparison.Ordinal);
if (decoratorStart < 0)
{
current.SHA = line.Trim();
}
else
{
current.SHA = line.Substring(0, decoratorStart).Trim();
current.IsMerged = ParseDecorators(current.Decorators, line.Substring(decoratorStart + 1));
if (!isHeadFounded)
isHeadFounded = current.IsMerged;
}
return;
}
if (current == null)
return;
if (line.StartsWith("tree ", StringComparison.Ordinal))
{
return;
}
else if (line.StartsWith("parent ", StringComparison.Ordinal))
{
current.Parents.Add(line.Substring("parent ".Length));
}
else if (line.StartsWith("author ", StringComparison.Ordinal))
{
Models.User user = Models.User.Invalid;
ulong time = 0;
Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time);
current.Author = user;
current.AuthorTime = time;
}
else if (line.StartsWith("committer ", StringComparison.Ordinal))
{
Models.User user = Models.User.Invalid;
ulong time = 0;
Models.Commit.ParseUserAndTime(line.Substring(10), ref user, ref time);
current.Committer = user;
current.CommitterTime = time;
}
else if (string.IsNullOrEmpty(current.Subject))
{
current.Subject = line.Trim();
}
else
{
current.Message += (line.Trim() + "\n");
}
_current.Parents.Add(data.Substring(0, idx));
_current.Parents.Add(data.Substring(idx + 1));
}
private bool ParseDecorators(List<Models.Decorator> decorators, string data)
private void ParseDecorators(string data)
{
bool isHeadOfCurrent = false;
if (data.Length < 3)
return;
var subs = data.Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries);
var subs = data.Split(',', StringSplitOptions.RemoveEmptyEntries);
foreach (var sub in subs)
{
var d = sub.Trim();
if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal))
{
decorators.Add(new Models.Decorator()
_current.Decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.Tag,
Name = d.Substring(15).Trim(),
Name = d.Substring(15),
});
}
else if (d.EndsWith("/HEAD", StringComparison.Ordinal))
@ -140,58 +108,55 @@ namespace SourceGit.Commands
}
else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal))
{
isHeadOfCurrent = true;
decorators.Add(new Models.Decorator()
_current.IsMerged = true;
_current.Decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.CurrentBranchHead,
Name = d.Substring(19).Trim(),
Name = d.Substring(19),
});
}
else if (d.Equals("HEAD"))
{
isHeadOfCurrent = true;
decorators.Add(new Models.Decorator()
_current.IsMerged = true;
_current.Decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.CurrentCommitHead,
Name = d.Trim(),
Name = d,
});
}
else if (d.StartsWith("refs/heads/", StringComparison.Ordinal))
{
decorators.Add(new Models.Decorator()
_current.Decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.LocalBranchHead,
Name = d.Substring(11).Trim(),
Name = d.Substring(11),
});
}
else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal))
{
decorators.Add(new Models.Decorator()
_current.Decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.RemoteBranchHead,
Name = d.Substring(13).Trim(),
Name = d.Substring(13),
});
}
}
decorators.Sort((l, r) =>
_current.Decorators.Sort((l, r) =>
{
if (l.Type != r.Type)
{
return (int)l.Type - (int)r.Type;
}
else
{
return l.Name.CompareTo(r.Name);
}
return string.Compare(l.Name, r.Name, StringComparison.Ordinal);
});
return isHeadOfCurrent;
if (_current.IsMerged && !_isHeadFounded)
_isHeadFounded = true;
}
private void MarkFirstMerged()
{
Args = $"log --since=\"{commits[commits.Count - 1].CommitterTimeStr}\" --format=\"%H\"";
Args = $"log --since=\"{_commits[_commits.Count - 1].CommitterTimeStr}\" --format=\"%H\"";
var rs = ReadToEnd();
var shas = rs.StdOut.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
@ -202,7 +167,7 @@ namespace SourceGit.Commands
foreach (var sha in shas)
set.Add(sha);
foreach (var c in commits)
foreach (var c in _commits)
{
if (set.Contains(c.SHA))
{
@ -211,5 +176,10 @@ namespace SourceGit.Commands
}
}
}
private List<Models.Commit> _commits = new List<Models.Commit>();
private Models.Commit _current = null;
private bool _findFirstMerged = false;
private bool _isHeadFounded = false;
}
}

View file

@ -5,22 +5,23 @@ namespace SourceGit.Commands
{
public partial class QueryRevisionObjects : Command
{
[GeneratedRegex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$")]
private static partial Regex REG_FORMAT();
private readonly List<Models.Object> objects = new List<Models.Object>();
public QueryRevisionObjects(string repo, string sha)
public QueryRevisionObjects(string repo, string sha, string parentFolder)
{
WorkingDirectory = repo;
Context = repo;
Args = $"ls-tree -r {sha}";
Args = $"ls-tree {sha}";
if (!string.IsNullOrEmpty(parentFolder))
Args += $" -- \"{parentFolder}\"";
}
public List<Models.Object> Result()
{
Exec();
return objects;
return _objects;
}
protected override void OnReadline(string line)
@ -50,7 +51,9 @@ namespace SourceGit.Commands
break;
}
objects.Add(obj);
_objects.Add(obj);
}
private List<Models.Object> _objects = new List<Models.Object>();
}
}

View file

@ -1,100 +1,50 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{
public class QuerySingleCommit : Command
{
private const string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----";
private const string GPGSIG_END = " -----END PGP SIGNATURE-----";
public QuerySingleCommit(string repo, string sha) {
public QuerySingleCommit(string repo, string sha)
{
WorkingDirectory = repo;
Context = repo;
Args = $"show --pretty=raw --decorate=full -s {sha}";
Args = $"show --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s -s {sha}";
}
public Models.Commit Result()
{
var succ = Exec();
if (!succ)
return null;
_commit.Message.Trim();
return _commit;
}
protected override void OnReadline(string line)
{
if (isSkipingGpgsig)
var rs = ReadToEnd();
if (rs.IsSuccess && !string.IsNullOrEmpty(rs.StdOut))
{
if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal))
isSkipingGpgsig = false;
return;
}
else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal))
{
isSkipingGpgsig = true;
return;
var commit = new Models.Commit();
var lines = rs.StdOut.Split('\n');
if (lines.Length < 8)
return null;
commit.SHA = lines[0];
if (!string.IsNullOrEmpty(lines[1]))
commit.Parents.AddRange(lines[1].Split(' ', StringSplitOptions.RemoveEmptyEntries));
if (!string.IsNullOrEmpty(lines[2]))
commit.IsMerged = ParseDecorators(commit.Decorators, lines[2]);
commit.Author = Models.User.FindOrAdd(lines[3]);
commit.AuthorTime = ulong.Parse(lines[4]);
commit.Committer = Models.User.FindOrAdd(lines[5]);
commit.CommitterTime = ulong.Parse(lines[6]);
commit.Subject = lines[7];
return commit;
}
if (line.StartsWith("commit ", StringComparison.Ordinal))
{
line = line.Substring(7);
var decoratorStart = line.IndexOf('(', StringComparison.Ordinal);
if (decoratorStart < 0)
{
_commit.SHA = line.Trim();
}
else
{
_commit.SHA = line.Substring(0, decoratorStart).Trim();
ParseDecorators(_commit.Decorators, line.Substring(decoratorStart + 1));
}
return;
}
if (line.StartsWith("tree ", StringComparison.Ordinal))
{
return;
}
else if (line.StartsWith("parent ", StringComparison.Ordinal))
{
_commit.Parents.Add(line.Substring("parent ".Length));
}
else if (line.StartsWith("author ", StringComparison.Ordinal))
{
Models.User user = Models.User.Invalid;
ulong time = 0;
Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time);
_commit.Author = user;
_commit.AuthorTime = time;
}
else if (line.StartsWith("committer ", StringComparison.Ordinal))
{
Models.User user = Models.User.Invalid;
ulong time = 0;
Models.Commit.ParseUserAndTime(line.Substring(10), ref user, ref time);
_commit.Committer = user;
_commit.CommitterTime = time;
}
else if (string.IsNullOrEmpty(_commit.Subject))
{
_commit.Subject = line.Trim();
}
else
{
_commit.Message += (line.Trim() + "\n");
}
return null;
}
private bool ParseDecorators(List<Models.Decorator> decorators, string data)
{
bool isHeadOfCurrent = false;
var subs = data.Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries);
var subs = data.Split(',', StringSplitOptions.RemoveEmptyEntries);
foreach (var sub in subs)
{
var d = sub.Trim();
@ -103,7 +53,7 @@ namespace SourceGit.Commands
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.Tag,
Name = d.Substring(15).Trim(),
Name = d.Substring(15),
});
}
else if (d.EndsWith("/HEAD", StringComparison.Ordinal))
@ -116,7 +66,7 @@ namespace SourceGit.Commands
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.CurrentBranchHead,
Name = d.Substring(19).Trim(),
Name = d.Substring(19),
});
}
else if (d.Equals("HEAD"))
@ -125,7 +75,7 @@ namespace SourceGit.Commands
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.CurrentCommitHead,
Name = d.Trim(),
Name = d,
});
}
else if (d.StartsWith("refs/heads/", StringComparison.Ordinal))
@ -133,7 +83,7 @@ namespace SourceGit.Commands
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.LocalBranchHead,
Name = d.Substring(11).Trim(),
Name = d.Substring(11),
});
}
else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal))
@ -141,7 +91,7 @@ namespace SourceGit.Commands
decorators.Add(new Models.Decorator()
{
Type = Models.DecoratorType.RemoteBranchHead,
Name = d.Substring(13).Trim(),
Name = d.Substring(13),
});
}
}
@ -149,19 +99,12 @@ namespace SourceGit.Commands
decorators.Sort((l, r) =>
{
if (l.Type != r.Type)
{
return (int)l.Type - (int)r.Type;
}
else
{
return l.Name.CompareTo(r.Name);
}
return string.Compare(l.Name, r.Name, StringComparison.Ordinal);
});
return isHeadOfCurrent;
}
private Models.Commit _commit = new Models.Commit();
private bool isSkipingGpgsig = false;
}
}

View file

@ -1,64 +1,48 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Collections.Generic;
namespace SourceGit.Commands
{
public partial class QueryStashes : Command
public class QueryStashes : Command
{
[GeneratedRegex(@"^Reflog: refs/(stash@\{\d+\}).*$")]
private static partial Regex REG_STASH();
public QueryStashes(string repo)
{
WorkingDirectory = repo;
Context = repo;
Args = "stash list --pretty=raw";
Args = "stash list --pretty=format:%H%n%ct%n%gd%n%s";
}
public List<Models.Stash> Result()
{
Exec();
if (_current != null)
_stashes.Add(_current);
return _stashes;
}
protected override void OnReadline(string line)
{
if (line.StartsWith("commit ", StringComparison.Ordinal))
switch (_nextLineIdx)
{
if (_current != null && !string.IsNullOrEmpty(_current.Name))
case 0:
_current = new Models.Stash() { SHA = line };
_stashes.Add(_current);
_current = new Models.Stash() { SHA = line.Substring(7, 8) };
return;
break;
case 1:
_current.Time = ulong.Parse(line);
break;
case 2:
_current.Name = line;
break;
case 3:
_current.Message = line;
break;
}
if (_current == null)
return;
if (line.StartsWith("Reflog: refs/stash@", StringComparison.Ordinal))
{
var match = REG_STASH().Match(line);
if (match.Success)
_current.Name = match.Groups[1].Value;
}
else if (line.StartsWith("Reflog message: ", StringComparison.Ordinal))
{
_current.Message = line.Substring(16);
}
else if (line.StartsWith("author ", StringComparison.Ordinal))
{
Models.User user = Models.User.Invalid;
ulong time = 0;
Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time);
_current.Author = user;
_current.Time = time;
}
_nextLineIdx++;
if (_nextLineIdx > 3)
_nextLineIdx = 0;
}
private readonly List<Models.Stash> _stashes = new List<Models.Stash>();
private Models.Stash _current = null;
private int _nextLineIdx = 0;
}
}

View file

@ -29,9 +29,10 @@ namespace SourceGit.Commands
}
}
public bool Update()
public bool Update(Action<string> outputHandler)
{
Args = $"submodule update --rebase --remote";
_outputHandler = outputHandler;
return Exec();
}

View file

@ -19,14 +19,5 @@ namespace SourceGit.Converters
return App.Current?.FindResource("Icons.Tree") as StreamGeometry;
}
});
public static readonly FuncValueConverter<Models.ChangeViewMode, bool> IsList =
new FuncValueConverter<Models.ChangeViewMode, bool>(v => v == Models.ChangeViewMode.List);
public static readonly FuncValueConverter<Models.ChangeViewMode, bool> IsGrid =
new FuncValueConverter<Models.ChangeViewMode, bool>(v => v == Models.ChangeViewMode.Grid);
public static readonly FuncValueConverter<Models.ChangeViewMode, bool> IsTree =
new FuncValueConverter<Models.ChangeViewMode, bool>(v => v == Models.ChangeViewMode.Tree);
}
}

View file

@ -1,38 +0,0 @@
using System.Collections.Generic;
using Avalonia.Collections;
using Avalonia.Data.Converters;
namespace SourceGit.Converters
{
public static class LauncherPageConverters
{
public static readonly FuncMultiValueConverter<object, bool> ToTabSeperatorVisible =
new FuncMultiValueConverter<object, bool>(v =>
{
if (v == null)
return false;
var array = new List<object>();
array.AddRange(v);
if (array.Count != 3)
return false;
var self = array[0] as ViewModels.LauncherPage;
if (self == null)
return false;
var selected = array[1] as ViewModels.LauncherPage;
var collections = array[2] as AvaloniaList<ViewModels.LauncherPage>;
if (selected != null && collections != null && (self == selected || collections.IndexOf(self) + 1 == collections.IndexOf(selected)))
{
return false;
}
else
{
return true;
}
});
}
}

View file

@ -30,17 +30,11 @@ namespace SourceGit.Converters
{
var theme = (string)value;
if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase))
{
return ThemeVariant.Light;
}
else if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase))
{
return ThemeVariant.Dark;
}
else
{
return ThemeVariant.Default;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

View file

@ -12,30 +12,20 @@ namespace SourceGit.Converters
new FuncValueConverter<WindowState, Thickness>(state =>
{
if (OperatingSystem.IsWindows() && state == WindowState.Maximized)
{
return new Thickness(6);
}
else if (OperatingSystem.IsLinux() && state != WindowState.Maximized)
{
return new Thickness(6);
}
else
{
return new Thickness(0);
}
});
public static readonly FuncValueConverter<WindowState, GridLength> ToTitleBarHeight =
new FuncValueConverter<WindowState, GridLength>(state =>
{
if (state == WindowState.Maximized)
{
return new GridLength(OperatingSystem.IsMacOS() ? 34 : 30);
}
else
{
return new GridLength(38);
}
});
public static readonly FuncValueConverter<WindowState, bool> IsNormal =

View file

@ -13,7 +13,6 @@ namespace SourceGit.Models
public User Committer { get; set; } = User.Invalid;
public ulong CommitterTime { get; set; } = 0;
public string Subject { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public List<string> Parents { get; set; } = new List<string>();
public List<Decorator> Decorators { get; set; } = new List<Decorator>();
public bool HasDecorators => Decorators.Count > 0;
@ -25,31 +24,8 @@ namespace SourceGit.Models
public string AuthorTimeShortStr => _utcStart.AddSeconds(AuthorTime).ToString("yyyy/MM/dd");
public string CommitterTimeShortStr => _utcStart.AddSeconds(CommitterTime).ToString("yyyy/MM/dd");
public bool IsCommitterVisible
{
get => Author != Committer || AuthorTime != CommitterTime;
}
public bool IsCurrentHead
{
get => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null;
}
public string FullMessage
{
get => string.IsNullOrWhiteSpace(Message) ? Subject : $"{Subject}\n\n{Message}";
}
public static void ParseUserAndTime(string data, ref User user, ref ulong time)
{
var userEndIdx = data.IndexOf('>', StringComparison.Ordinal);
if (userEndIdx < 0)
return;
var timeEndIdx = data.IndexOf(' ', userEndIdx + 2);
user = User.FindOrAdd(data.Substring(0, userEndIdx));
time = timeEndIdx < 0 ? 0 : ulong.Parse(data.Substring(userEndIdx + 2, timeEndIdx - userEndIdx - 2));
}
public bool IsCommitterVisible => Author != Committer || AuthorTime != CommitterTime;
public bool IsCurrentHead => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null;
private static readonly DateTime _utcStart = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
}

View file

@ -2,11 +2,23 @@
using System.Collections.Generic;
using Avalonia;
using Avalonia.Media;
namespace SourceGit.Models
{
public class CommitGraph
{
public static readonly Pen[] Pens = [
new Pen(Brushes.Orange, 2),
new Pen(Brushes.ForestGreen, 2),
new Pen(Brushes.Gold, 2),
new Pen(Brushes.Magenta, 2),
new Pen(Brushes.Red, 2),
new Pen(Brushes.Gray, 2),
new Pen(Brushes.Turquoise, 2),
new Pen(Brushes.Olive, 2),
];
public class Path
{
public List<Point> Points = new List<Point>();
@ -101,12 +113,12 @@ namespace SourceGit.Models
public List<Link> Links { get; set; } = new List<Link>();
public List<Dot> Dots { get; set; } = new List<Dot>();
public static CommitGraph Parse(List<Commit> commits, double rowHeight, int colorCount)
public static CommitGraph Parse(List<Commit> commits, int colorCount)
{
double UNIT_WIDTH = 12;
double HALF_WIDTH = 6;
double UNIT_HEIGHT = rowHeight;
double HALF_HEIGHT = rowHeight / 2;
double UNIT_HEIGHT = 28;
double HALF_HEIGHT = 14;
var temp = new CommitGraph();
var unsolved = new List<PathHelper>();

View file

@ -2,6 +2,7 @@
using System.Text;
using System.Text.RegularExpressions;
using Avalonia;
using Avalonia.Media.Imaging;
namespace SourceGit.Models
@ -62,6 +63,7 @@ namespace SourceGit.Models
{
public string File { get; set; } = string.Empty;
public List<TextDiffLine> Lines { get; set; } = new List<TextDiffLine>();
public Vector SyncScrollOffset { get; set; } = Vector.Zero;
public int MaxLineNumber = 0;
public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output)
@ -568,8 +570,11 @@ namespace SourceGit.Models
public Bitmap Old { get; set; } = null;
public Bitmap New { get; set; } = null;
public string OldSize => Old != null ? $"{Old.PixelSize.Width} x {Old.PixelSize.Height}" : "0 x 0";
public string NewSize => New != null ? $"{New.PixelSize.Width} x {New.PixelSize.Height}" : "0 x 0";
public long OldFileSize { get; set; } = 0;
public long NewFileSize { get; set; } = 0;
public string OldImageSize => Old != null ? $"{Old.PixelSize.Width} x {Old.PixelSize.Height}" : "0 x 0";
public string NewImageSize => New != null ? $"{New.PixelSize.Width} x {New.PixelSize.Height}" : "0 x 0";
}
public class NoOrEOLChange
@ -582,10 +587,16 @@ namespace SourceGit.Models
public string New { get; set; } = string.Empty;
}
public class SubmoduleRevision
{
public Commit Commit { get; set; } = null;
public string FullMessage { get; set; } = string.Empty;
}
public class SubmoduleDiff
{
public Commit Old { get; set; } = null;
public Commit New { get; set; } = null;
public SubmoduleRevision Old { get; set; } = null;
public SubmoduleRevision New { get; set; } = null;
}
public class DiffResult

View file

@ -1,185 +0,0 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Models
{
public class FileTreeNode
{
public string FullPath { get; set; } = string.Empty;
public bool IsFolder { get; set; } = false;
public bool IsExpanded { get; set; } = false;
public object Backend { get; set; } = null;
public List<FileTreeNode> Children { get; set; } = new List<FileTreeNode>();
public static List<FileTreeNode> Build(List<Change> changes, bool expanded)
{
var nodes = new List<FileTreeNode>();
var folders = new Dictionary<string, FileTreeNode>();
foreach (var c in changes)
{
var sepIdx = c.Path.IndexOf('/', StringComparison.Ordinal);
if (sepIdx == -1)
{
nodes.Add(new FileTreeNode()
{
FullPath = c.Path,
Backend = c,
IsFolder = false,
IsExpanded = false
});
}
else
{
FileTreeNode lastFolder = null;
var start = 0;
while (sepIdx != -1)
{
var folder = c.Path.Substring(0, sepIdx);
if (folders.TryGetValue(folder, out var value))
{
lastFolder = value;
}
else if (lastFolder == null)
{
lastFolder = new FileTreeNode()
{
FullPath = folder,
Backend = null,
IsFolder = true,
IsExpanded = expanded
};
nodes.Add(lastFolder);
folders.Add(folder, lastFolder);
}
else
{
var cur = new FileTreeNode()
{
FullPath = folder,
Backend = null,
IsFolder = true,
IsExpanded = expanded
};
folders.Add(folder, cur);
lastFolder.Children.Add(cur);
lastFolder = cur;
}
start = sepIdx + 1;
sepIdx = c.Path.IndexOf('/', start);
}
lastFolder.Children.Add(new FileTreeNode()
{
FullPath = c.Path,
Backend = c,
IsFolder = false,
IsExpanded = false
});
}
}
folders.Clear();
Sort(nodes);
return nodes;
}
public static List<FileTreeNode> Build(List<Object> files, bool expanded)
{
var nodes = new List<FileTreeNode>();
var folders = new Dictionary<string, FileTreeNode>();
foreach (var f in files)
{
var sepIdx = f.Path.IndexOf('/', StringComparison.Ordinal);
if (sepIdx == -1)
{
nodes.Add(new FileTreeNode()
{
FullPath = f.Path,
Backend = f,
IsFolder = false,
IsExpanded = false
});
}
else
{
FileTreeNode lastFolder = null;
var start = 0;
while (sepIdx != -1)
{
var folder = f.Path.Substring(0, sepIdx);
if (folders.TryGetValue(folder, out var value))
{
lastFolder = value;
}
else if (lastFolder == null)
{
lastFolder = new FileTreeNode()
{
FullPath = folder,
Backend = null,
IsFolder = true,
IsExpanded = expanded
};
nodes.Add(lastFolder);
folders.Add(folder, lastFolder);
}
else
{
var cur = new FileTreeNode()
{
FullPath = folder,
Backend = null,
IsFolder = true,
IsExpanded = expanded
};
folders.Add(folder, cur);
lastFolder.Children.Add(cur);
lastFolder = cur;
}
start = sepIdx + 1;
sepIdx = f.Path.IndexOf('/', start);
}
lastFolder.Children.Add(new FileTreeNode()
{
FullPath = f.Path,
Backend = f,
IsFolder = false,
IsExpanded = false
});
}
}
folders.Clear();
Sort(nodes);
return nodes;
}
private static void Sort(List<FileTreeNode> nodes)
{
nodes.Sort((l, r) =>
{
if (l.IsFolder == r.IsFolder)
{
return l.FullPath.CompareTo(r.FullPath);
}
else
{
return l.IsFolder ? -1 : 1;
}
});
foreach (var node in nodes)
{
if (node.Children.Count > 1)
Sort(node.Children);
}
}
}
}

View file

@ -2,24 +2,18 @@
namespace SourceGit.Models
{
public class GPGFormat(string name, string value, string desc)
public class GPGFormat(string name, string value, string desc, string program, bool needFindProgram)
{
public string Name { get; set; } = name;
public string Value { get; set; } = value;
public string Desc { get; set; } = desc;
public string Program { get; set; } = program;
public bool NeedFindProgram { get; set; } = needFindProgram;
public static readonly GPGFormat OPENPGP = new GPGFormat("OPENPGP", "openpgp", "DEFAULT");
public static readonly GPGFormat SSH = new GPGFormat("SSH", "ssh", "Git >= 2.34.0");
public static readonly List<GPGFormat> Supported = new List<GPGFormat>() {
OPENPGP,
SSH,
};
public bool Equals(GPGFormat other)
{
return Value == other.Value;
}
public static readonly List<GPGFormat> Supported = [
new GPGFormat("OPENPGP", "openpgp", "DEFAULT", "gpg", true),
new GPGFormat("X.509", "x509", "", "gpgsm", true),
new GPGFormat("SSH", "ssh", "Requires Git >= 2.34.0", "ssh-keygen", false),
];
}
}

View file

@ -10,6 +10,7 @@ namespace SourceGit.Models
public static readonly List<Locale> Supported = new List<Locale>() {
new Locale("English", "en_US"),
new Locale("简体中文", "zh_CN"),
new Locale("繁體中文", "zh_TW"),
};
public Locale(string name, string key)

View file

@ -8,7 +8,6 @@ namespace SourceGit.Models
public string Name { get; set; } = "";
public string SHA { get; set; } = "";
public User Author { get; set; } = User.Invalid;
public ulong Time { get; set; } = 0;
public string Message { get; set; } = "";

View file

@ -42,23 +42,15 @@ namespace SourceGit.Models
public void Select(IEnumerable<TModel> items)
{
var sets = new HashSet<TModel>();
foreach (var item in items)
sets.Add(item);
using (BatchUpdate())
{
Clear();
int num = _source.Rows.Count;
for (int i = 0; i < num; ++i)
foreach (var selected in items)
{
var m = _source.Rows[i].Model as TModel;
if (m != null && sets.Contains(m))
{
var idx = _source.Rows.RowIndexToModelIndex(i);
var idx = GetModelIndex(_source.Items, selected, IndexPath.Unselected);
if (!idx.Equals(IndexPath.Unselected))
Select(idx);
}
}
}
}
@ -431,6 +423,30 @@ namespace SourceGit.Models
return _childrenGetter?.Invoke(node);
}
private IndexPath GetModelIndex(IEnumerable<TModel> collection, TModel model, IndexPath parent)
{
int i = 0;
foreach (var item in collection)
{
var index = parent.Append(i);
if (item != null && item == model)
return index;
var children = GetChildren(item);
if (children != null)
{
var findInChildren = GetModelIndex(children, model, index);
if (!findInChildren.Equals(IndexPath.Unselected))
return findInChildren;
}
i++;
}
return IndexPath.Unselected;
}
private bool HasChildren(IRow row)
{
var children = GetChildren(row.Model as TModel);

View file

@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System;
using System.Collections.Concurrent;
namespace SourceGit.Models
{
@ -27,8 +28,8 @@ namespace SourceGit.Models
{
return _caches.GetOrAdd(data, key =>
{
var nameEndIdx = key.IndexOf('<', System.StringComparison.Ordinal);
var name = nameEndIdx >= 2 ? key.Substring(0, nameEndIdx - 1) : string.Empty;
var nameEndIdx = key.IndexOf('±', StringComparison.Ordinal);
var name = nameEndIdx > 0 ? key.Substring(0, nameEndIdx) : string.Empty;
var email = key.Substring(nameEndIdx + 1);
return new User() { Name = name, Email = email };

View file

@ -9,7 +9,6 @@
<StreamGeometry x:Key="Icons.TriangleLeft">M30 0 30 30 0 15z</StreamGeometry>
<StreamGeometry x:Key="Icons.TriangleRight">M0 0 0 30 30 15z</StreamGeometry>
<StreamGeometry x:Key="Icons.Menu">M192 192m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM192 512m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM192 832m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM864 160H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 480H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 800H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32z</StreamGeometry>
<StreamGeometry x:Key="Icons.Goback">M512 945c-238 0-433-195-433-433S274 79 512 79c238 0 433 195 433 433S750 945 512 945M512 0C228 0 0 228 0 512s228 512 512 512 512-228 512-512-228-512-512-512zM752 477H364l128-128a38 38 0 000-55 38 38 0 00-55 0l-185 183a55 55 0 00-16 39c0 16 6 30 16 39l185 185a39 39 0 0028 12 34 34 0 0028-14 38 38 0 000-55l-128-128h386c22 0 41-18 41-39a39 39 0 00-39-39</StreamGeometry>
<StreamGeometry x:Key="Icons.Error">M576 832C576 867 547 896 512 896 477 896 448 867 448 832 448 797 477 768 512 768 547 768 576 797 576 832ZM512 256C477 256 448 285 448 320L448 640C448 675 477 704 512 704 547 704 576 675 576 640L576 320C576 285 547 256 512 256ZM1024 896C1024 967 967 1024 896 1024L128 1024C57 1024 0 967 0 896 0 875 5 855 14 837L14 837 398 69 398 69C420 28 462 0 512 0 562 0 604 28 626 69L1008 835C1018 853 1024 874 1024 896ZM960 896C960 885 957 875 952 865L952 864 951 863 569 98C557 77 536 64 512 64 488 64 466 77 455 99L452 105 92 825 93 825 71 867C66 876 64 886 64 896 64 931 93 960 128 960L896 960C931 960 960 931 960 896Z</StreamGeometry>
<StreamGeometry x:Key="Icons.Conflict">M608 0q48 0 88 23t63 63 23 87v70h55q35 0 67 14t57 38 38 57 14 67V831q0 34-14 66t-38 57-57 38-67 13H426q-34 0-66-13t-57-38-38-57-14-66v-70h-56q-34 0-66-14t-57-38-38-57-13-67V174q0-47 23-87T109 23 196 0h412m175 244H426q-46 0-86 22T278 328t-26 85v348H608q47 0 86-22t63-62 25-85l1-348m-269 318q18 0 31 13t13 31-13 31-31 13-31-13-13-31 13-31 31-13m0-212q13 0 22 9t11 22v125q0 14-9 23t-22 10-23-7-11-22l-1-126q0-13 10-23t23-10z</StreamGeometry>
<StreamGeometry x:Key="Icons.Plus">m186 532 287 0 0 287c0 11 9 20 20 20s20-9 20-20l0-287 287 0c11 0 20-9 20-20s-9-20-20-20l-287 0 0-287c0-11-9-20-20-20s-20 9-20 20l0 287-287 0c-11 0-20 9-20 20s9 20 20 20z</StreamGeometry>
@ -98,4 +97,5 @@
<StreamGeometry x:Key="Icons.Lines.Incr">M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l132 0 0-128 64 0 0 128 132 0 0 64-132 0 0 128-64 0 0-128-132 0Z</StreamGeometry>
<StreamGeometry x:Key="Icons.Lines.Decr">M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l328 0 0 64-328 0Z</StreamGeometry>
<StreamGeometry x:Key="Icons.Compare">M645 448l64 64 220-221L704 64l-64 64 115 115H128v90h628zM375 576l-64-64-220 224L314 960l64-64-116-115H896v-90H262z</StreamGeometry>
<StreamGeometry x:Key="Icons.WordWrap">M248 221a77 77 0 00-30-21c-18-7-40-10-68-5a224 224 0 00-45 13c-5 2-10 5-15 8l-3 2v68l11-9c10-8 21-14 34-19 13-5 26-7 39-7 12 0 21 3 28 10 6 6 9 16 9 29l-62 9c-14 2-26 6-36 11a80 80 0 00-25 20c-7 8-12 17-15 27-6 21-6 44 1 65a70 70 0 0041 43c10 4 21 6 34 6a80 80 0 0063-28v22h64V298c0-16-2-31-6-44a91 91 0 00-18-33zm-41 121v15c0 8-1 15-4 22a48 48 0 01-24 29 44 44 0 01-33 2 29 29 0 01-10-6 25 25 0 01-6-9 30 30 0 01-2-12c0-5 1-9 2-14a21 21 0 015-9 28 28 0 0110-7 83 83 0 0120-5l42-6zm323-68a144 144 0 00-16-42 87 87 0 00-28-29 75 75 0 00-41-11 73 73 0 00-44 14c-6 5-12 11-17 17V64H326v398h59v-18c8 10 18 17 30 21 6 2 13 3 21 3 16 0 31-4 43-11 12-7 23-18 31-31a147 147 0 0019-46 248 248 0 006-57c0-17-2-33-5-49zm-55 49c0 15-1 28-4 39-2 11-6 20-10 27a41 41 0 01-15 15 37 37 0 01-36 1 44 44 0 01-13-12 59 59 0 01-9-18A76 76 0 01384 352v-33c0-10 1-20 4-29 2-8 6-15 10-22a43 43 0 0115-13 37 37 0 0119-5 35 35 0 0132 18c4 6 7 14 9 23 2 9 3 20 3 31zM154 634a58 58 0 0120-15c14-6 35-7 49-1 7 3 13 6 20 12l21 17V572l-6-4a124 124 0 00-58-14c-20 0-38 4-54 11-16 7-30 17-41 30-12 13-20 29-26 46-6 17-9 36-9 57 0 18 3 36 8 52 6 16 14 30 24 42 10 12 23 21 38 28 15 7 32 10 50 10 15 0 28-2 39-5 11-3 21-8 30-14l5-4v-57l-13 6a26 26 0 01-5 2c-3 1-6 2-8 3-2 1-15 6-15 6-4 2-9 3-14 4a63 63 0 01-38-4 53 53 0 01-20-14 70 70 0 01-13-24 111 111 0 01-5-34c0-13 2-26 5-36 3-10 8-19 14-26zM896 384h-256V320h288c21 1 32 12 32 32v384c0 18-12 32-32 32H504l132 133-45 45-185-185c-16-21-16-25 0-45l185-185L637 576l-128 128H896V384z</StreamGeometry>
</ResourceDictionary>

View file

@ -93,10 +93,7 @@
<x:String x:Key="Text.CommitDetail.Changes.Search" xml:space="preserve">Search Changes ...</x:String>
<x:String x:Key="Text.CommitDetail.Files" xml:space="preserve">FILES</x:String>
<x:String x:Key="Text.CommitDetail.Files.LFS" xml:space="preserve">LFS File</x:String>
<x:String x:Key="Text.CommitDetail.Files.Search" xml:space="preserve">Search Files ...</x:String>
<x:String x:Key="Text.CommitDetail.Files.Submodule" xml:space="preserve">Submodule</x:String>
<x:String x:Key="Text.CommitDetail.Files.Tag" xml:space="preserve">Tag</x:String>
<x:String x:Key="Text.CommitDetail.Files.Tree" xml:space="preserve">Tree</x:String>
<x:String x:Key="Text.CommitDetail.Info" xml:space="preserve">INFORMATION</x:String>
<x:String x:Key="Text.CommitDetail.Info.Author" xml:space="preserve">AUTHOR</x:String>
<x:String x:Key="Text.CommitDetail.Info.Changed" xml:space="preserve">CHANGED</x:String>
@ -166,6 +163,7 @@
<x:String x:Key="Text.Diff.Submodule" xml:space="preserve">SUBMODULE</x:String>
<x:String x:Key="Text.Diff.Submodule.New" xml:space="preserve">NEW</x:String>
<x:String x:Key="Text.Diff.SyntaxHighlight" xml:space="preserve">Syntax Highlighting</x:String>
<x:String x:Key="Text.Diff.ToggleWordWrap" xml:space="preserve">Line Word Wrap</x:String>
<x:String x:Key="Text.Diff.UseMerger" xml:space="preserve">Open In Merge Tool</x:String>
<x:String x:Key="Text.Diff.VisualLines.Decr" xml:space="preserve">Decrease Number of Visible Lines</x:String>
<x:String x:Key="Text.Diff.VisualLines.Incr" xml:space="preserve">Increase Number of Visible Lines</x:String>
@ -285,6 +283,7 @@
<x:String x:Key="Text.Paste" xml:space="preserve">Paste</x:String>
<x:String x:Key="Text.Preference" xml:space="preserve">Preference</x:String>
<x:String x:Key="Text.Preference.Appearance" xml:space="preserve">APPEARANCE</x:String>
<x:String x:Key="Text.Preference.Appearance.ColorOverrides" xml:space="preserve">Custom Color Schema</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFont" xml:space="preserve">Default Font</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFontSize" xml:space="preserve">Default Font Size</x:String>
<x:String x:Key="Text.Preference.Appearance.MonospaceFont" xml:space="preserve">Monospace Font</x:String>
@ -314,7 +313,7 @@
<x:String x:Key="Text.Preference.GPG.CommitEnabled" xml:space="preserve">Commit GPG signing</x:String>
<x:String x:Key="Text.Preference.GPG.TagEnabled" xml:space="preserve">Tag GPG signing</x:String>
<x:String x:Key="Text.Preference.GPG.Format" xml:space="preserve">GPG Format</x:String>
<x:String x:Key="Text.Preference.GPG.Path" xml:space="preserve">Install Path</x:String>
<x:String x:Key="Text.Preference.GPG.Path" xml:space="preserve">Program Install Path</x:String>
<x:String x:Key="Text.Preference.GPG.Path.Placeholder" xml:space="preserve">Input path for installed gpg program</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey" xml:space="preserve">User Signing Key</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey.Placeholder" xml:space="preserve">User's gpg signing key</x:String>
@ -385,7 +384,9 @@
<x:String x:Key="Text.Repository.Remotes.Add" xml:space="preserve">ADD REMOTE</x:String>
<x:String x:Key="Text.Repository.Resolve" xml:space="preserve">RESOLVE</x:String>
<x:String x:Key="Text.Repository.Search" xml:space="preserve">Search Commit</x:String>
<x:String x:Key="Text.Repository.SearchTip" xml:space="preserve">Search Author/Committer/Message/SHA</x:String>
<x:String x:Key="Text.Repository.Search.By" xml:space="preserve">Search By</x:String>
<x:String x:Key="Text.Repository.Search.ByBaseInfo" xml:space="preserve">Information</x:String>
<x:String x:Key="Text.Repository.Search.ByFile" xml:space="preserve">File</x:String>
<x:String x:Key="Text.Repository.Statistics" xml:space="preserve">Statistics</x:String>
<x:String x:Key="Text.Repository.Submodules" xml:space="preserve">SUBMODULES</x:String>
<x:String x:Key="Text.Repository.Submodules.Add" xml:space="preserve">ADD SUBMODULE</x:String>
@ -458,6 +459,8 @@
<x:String x:Key="Text.TagCM.Delete" xml:space="preserve">Delete${0}$</x:String>
<x:String x:Key="Text.TagCM.Push" xml:space="preserve">Push${0}$</x:String>
<x:String x:Key="Text.URL" xml:space="preserve">URL :</x:String>
<x:String x:Key="Text.UpdateSubmodules" xml:space="preserve">Update Submodules</x:String>
<x:String x:Key="Text.UpdateSubmodules.Tip" xml:space="preserve">Run `submodule update` command for this repository.</x:String>
<x:String x:Key="Text.Warn" xml:space="preserve">Warning</x:String>
<x:String x:Key="Text.Welcome.AddRootFolder" xml:space="preserve">Create Group</x:String>
<x:String x:Key="Text.Welcome.AddSubFolder" xml:space="preserve">Create Sub-Group</x:String>

View file

@ -96,10 +96,7 @@
<x:String x:Key="Text.CommitDetail.Changes.Search" xml:space="preserve">查找变更...</x:String>
<x:String x:Key="Text.CommitDetail.Files" xml:space="preserve">文件列表</x:String>
<x:String x:Key="Text.CommitDetail.Files.LFS" xml:space="preserve">LFS文件</x:String>
<x:String x:Key="Text.CommitDetail.Files.Search" xml:space="preserve">查找文件...</x:String>
<x:String x:Key="Text.CommitDetail.Files.Submodule" xml:space="preserve">子模块</x:String>
<x:String x:Key="Text.CommitDetail.Files.Tag" xml:space="preserve">标签文件</x:String>
<x:String x:Key="Text.CommitDetail.Files.Tree" xml:space="preserve">子树</x:String>
<x:String x:Key="Text.CommitDetail.Info" xml:space="preserve">基本信息</x:String>
<x:String x:Key="Text.CommitDetail.Info.Author" xml:space="preserve">修改者</x:String>
<x:String x:Key="Text.CommitDetail.Info.Changed" xml:space="preserve">变更列表</x:String>
@ -169,6 +166,7 @@
<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.SyntaxHighlight" xml:space="preserve">语法高亮</x:String>
<x:String x:Key="Text.Diff.ToggleWordWrap" xml:space="preserve">自动换行</x:String>
<x:String x:Key="Text.Diff.UseMerger" xml:space="preserve">使用外部合并工具查看</x:String>
<x:String x:Key="Text.Diff.VisualLines.Decr" xml:space="preserve">减少可见的行数</x:String>
<x:String x:Key="Text.Diff.VisualLines.Incr" xml:space="preserve">增加可见的行数</x:String>
@ -288,6 +286,7 @@
<x:String x:Key="Text.Paste" xml:space="preserve">粘贴</x:String>
<x:String x:Key="Text.Preference" xml:space="preserve">偏好设置</x:String>
<x:String x:Key="Text.Preference.Appearance" xml:space="preserve">外观配置</x:String>
<x:String x:Key="Text.Preference.Appearance.ColorOverrides" xml:space="preserve">自定义配色文件</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFont" xml:space="preserve">缺省字体</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFontSize" xml:space="preserve">默认字体大小</x:String>
<x:String x:Key="Text.Preference.Appearance.MonospaceFont" xml:space="preserve">等宽字体</x:String>
@ -316,9 +315,9 @@
<x:String x:Key="Text.Preference.GPG" xml:space="preserve">GPG签名</x:String>
<x:String x:Key="Text.Preference.GPG.CommitEnabled" xml:space="preserve">启用提交签名</x:String>
<x:String x:Key="Text.Preference.GPG.TagEnabled" xml:space="preserve">启用标签签名</x:String>
<x:String x:Key="Text.Preference.GPG.Format" xml:space="preserve">GPG签名格式</x:String>
<x:String x:Key="Text.Preference.GPG.Path" xml:space="preserve">可执行文件位置</x:String>
<x:String x:Key="Text.Preference.GPG.Path.Placeholder" xml:space="preserve">gpg.exe所在路径</x:String>
<x:String x:Key="Text.Preference.GPG.Format" xml:space="preserve">签名格式</x:String>
<x:String x:Key="Text.Preference.GPG.Path" xml:space="preserve">签名程序位置</x:String>
<x:String x:Key="Text.Preference.GPG.Path.Placeholder" xml:space="preserve">签名程序所在路径</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey" xml:space="preserve">用户签名KEY</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey.Placeholder" xml:space="preserve">输入签名提交所使用的KEY</x:String>
<x:String x:Key="Text.Preference.Merger" xml:space="preserve">外部合并工具</x:String>
@ -388,7 +387,9 @@
<x:String x:Key="Text.Repository.Remotes.Add" xml:space="preserve">添加远程</x:String>
<x:String x:Key="Text.Repository.Resolve" xml:space="preserve">解决冲突</x:String>
<x:String x:Key="Text.Repository.Search" xml:space="preserve">查找提交</x:String>
<x:String x:Key="Text.Repository.SearchTip" xml:space="preserve">支持搜索作者/提交者/主题/指纹</x:String>
<x:String x:Key="Text.Repository.Search.By" xml:space="preserve">搜索途径</x:String>
<x:String x:Key="Text.Repository.Search.ByBaseInfo" xml:space="preserve">摘要</x:String>
<x:String x:Key="Text.Repository.Search.ByFile" xml:space="preserve">文件</x:String>
<x:String x:Key="Text.Repository.Statistics" xml:space="preserve">提交统计</x:String>
<x:String x:Key="Text.Repository.Submodules" xml:space="preserve">子模块列表</x:String>
<x:String x:Key="Text.Repository.Submodules.Add" xml:space="preserve">添加子模块</x:String>
@ -461,6 +462,8 @@
<x:String x:Key="Text.TagCM.Delete" xml:space="preserve">删除${0}$</x:String>
<x:String x:Key="Text.TagCM.Push" xml:space="preserve">推送${0}$</x:String>
<x:String x:Key="Text.URL" xml:space="preserve">仓库地址 </x:String>
<x:String x:Key="Text.UpdateSubmodules" xml:space="preserve">更新子模块</x:String>
<x:String x:Key="Text.UpdateSubmodules.Tip" xml:space="preserve">为此仓库执行`submodule update`命令,更新所有的子模块。</x:String>
<x:String x:Key="Text.Warn" xml:space="preserve">警告</x:String>
<x:String x:Key="Text.Welcome.AddRootFolder" xml:space="preserve">新建分组</x:String>
<x:String x:Key="Text.Welcome.AddSubFolder" xml:space="preserve">新建子分组</x:String>

View file

@ -0,0 +1,500 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="en_US.axaml"/>
</ResourceDictionary.MergedDictionaries>
<x:String x:Key="Text.About" xml:space="preserve">關於軟體</x:String>
<x:String x:Key="Text.About.Menu" xml:space="preserve">關於本軟體</x:String>
<x:String x:Key="Text.About.BuildWith" xml:space="preserve">• 專案依賴於 </x:String>
<x:String x:Key="Text.About.Copyright" xml:space="preserve">© 2024 sourcegit-scm</x:String>
<x:String x:Key="Text.About.Editor" xml:space="preserve">• 文字編輯器使用 </x:String>
<x:String x:Key="Text.About.Fonts" xml:space="preserve">• 等寬字型來自於 </x:String>
<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.Apply" xml:space="preserve">應用補丁(apply)</x:String>
<x:String x:Key="Text.Apply.Error" xml:space="preserve">錯誤</x:String>
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">輸出錯誤,並終止應用補丁</x:String>
<x:String x:Key="Text.Apply.ErrorAll" xml:space="preserve">更多錯誤</x:String>
<x:String x:Key="Text.Apply.ErrorAll.Desc" xml:space="preserve">與【錯誤】級別相似,但輸出內容更多</x:String>
<x:String x:Key="Text.Apply.File" xml:space="preserve">補丁檔案 </x:String>
<x:String x:Key="Text.Apply.File.Placeholder" xml:space="preserve">選擇補丁檔案</x:String>
<x:String x:Key="Text.Apply.IgnoreWS" xml:space="preserve">忽略空白符號</x:String>
<x:String x:Key="Text.Apply.NoWarn" xml:space="preserve">忽略</x:String>
<x:String x:Key="Text.Apply.NoWarn.Desc" xml:space="preserve">關閉所有警告</x:String>
<x:String x:Key="Text.Apply.Title" xml:space="preserve">應用補丁</x:String>
<x:String x:Key="Text.Apply.Warn" xml:space="preserve">警告</x:String>
<x:String x:Key="Text.Apply.Warn.Desc" xml:space="preserve">應用補丁,輸出關於空白符的警告</x:String>
<x:String x:Key="Text.Apply.WS" xml:space="preserve">空白符號處理 </x:String>
<x:String x:Key="Text.Archive" xml:space="preserve">存檔(archive) ...</x:String>
<x:String x:Key="Text.Archive.File" xml:space="preserve">存檔檔案路徑:</x:String>
<x:String x:Key="Text.Archive.File.Placeholder" xml:space="preserve">選擇存檔檔案的存放路徑</x:String>
<x:String x:Key="Text.Archive.Revision" xml:space="preserve">指定的提交:</x:String>
<x:String x:Key="Text.Archive.Title" xml:space="preserve">存檔</x:String>
<x:String x:Key="Text.AssumeUnchanged" xml:space="preserve">不跟蹤更改的檔案</x:String>
<x:String x:Key="Text.AssumeUnchanged.Empty" xml:space="preserve">沒有不跟蹤更改的檔案</x:String>
<x:String x:Key="Text.AssumeUnchanged.Remove" xml:space="preserve">移除</x:String>
<x:String x:Key="Text.BinaryNotSupported" xml:space="preserve">二進位制檔案不支援該操作!!!</x:String>
<x:String x:Key="Text.Blame" xml:space="preserve">逐行追溯(blame)</x:String>
<x:String x:Key="Text.BlameTypeNotSupported" xml:space="preserve">選中檔案不支援該操作!!!</x:String>
<x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">檢出(checkout)${0}$</x:String>
<x:String x:Key="Text.BranchCM.CompareWithHead" xml:space="preserve">與當前HEAD比較</x:String>
<x:String x:Key="Text.BranchCM.CompareWithWorktree" xml:space="preserve">與本地工作樹比較</x:String>
<x:String x:Key="Text.BranchCM.CopyName" xml:space="preserve">複製分支名</x:String>
<x:String x:Key="Text.BranchCM.Delete" xml:space="preserve">刪除${0}$</x:String>
<x:String x:Key="Text.BranchCM.DeleteMultiBranches" xml:space="preserve">刪除選中的 {0} 個分支</x:String>
<x:String x:Key="Text.BranchCM.DiscardAll" xml:space="preserve">放棄所有更改</x:String>
<x:String x:Key="Text.BranchCM.FastForward" xml:space="preserve">快進(fast-forward)到${0}$</x:String>
<x:String x:Key="Text.BranchCM.Finish" xml:space="preserve">GIT工作流 - 完成${0}$</x:String>
<x:String x:Key="Text.BranchCM.Merge" xml:space="preserve">合併${0}$到${1}$</x:String>
<x:String x:Key="Text.BranchCM.Pull" xml:space="preserve">拉回(pull)${0}$</x:String>
<x:String x:Key="Text.BranchCM.PullInto" xml:space="preserve">拉回(pull)${0}$內容至${1}$</x:String>
<x:String x:Key="Text.BranchCM.Push" xml:space="preserve">推送(push)${0}$</x:String>
<x:String x:Key="Text.BranchCM.Rebase" xml:space="preserve">變基(rebase)${0}$分支至${1}$</x:String>
<x:String x:Key="Text.BranchCM.Rename" xml:space="preserve">重新命名${0}$</x:String>
<x:String x:Key="Text.BranchCM.Tracking" xml:space="preserve">切換上游分支...</x:String>
<x:String x:Key="Text.BranchCM.UnsetUpstream" xml:space="preserve">取消追蹤</x:String>
<x:String x:Key="Text.Bytes" xml:space="preserve">位元組</x:String>
<x:String x:Key="Text.Cancel" xml:space="preserve">取 消</x:String>
<x:String x:Key="Text.ChangeDisplayMode" xml:space="preserve">切換變更顯示模式</x:String>
<x:String x:Key="Text.ChangeDisplayMode.Grid" xml:space="preserve">檔名+路徑列表模式</x:String>
<x:String x:Key="Text.ChangeDisplayMode.List" xml:space="preserve">全路徑列表模式</x:String>
<x:String x:Key="Text.ChangeDisplayMode.Tree" xml:space="preserve">檔案目錄樹形結構模式</x:String>
<x:String x:Key="Text.Checkout" xml:space="preserve">檢出(checkout)分支</x:String>
<x:String x:Key="Text.Checkout.Commit" xml:space="preserve">檢出(checkout)提交</x:String>
<x:String x:Key="Text.Checkout.Commit.Warning" xml:space="preserve">注意執行該操作後當前HEAD會變為遊離(detached)狀態!</x:String>
<x:String x:Key="Text.Checkout.Commit.Target" xml:space="preserve">提交 </x:String>
<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">挑選(cherry-pick)此提交</x:String>
<x:String x:Key="Text.CherryPick.Commit" xml:space="preserve">提交ID </x:String>
<x:String x:Key="Text.CherryPick.CommitChanges" xml:space="preserve">提交變化</x:String>
<x:String x:Key="Text.CherryPick.Title" xml:space="preserve">挑選提交</x:String>
<x:String x:Key="Text.ClearStashes" xml:space="preserve">丟棄儲藏確認</x:String>
<x:String x:Key="Text.ClearStashes.Message" xml:space="preserve">您正在丟棄所有的儲藏,一經操作,無法回退,是否繼續?</x:String>
<x:String x:Key="Text.Clone" xml:space="preserve">克隆遠端倉庫</x:String>
<x:String x:Key="Text.Clone.AdditionalParam" xml:space="preserve">額外引數 </x:String>
<x:String x:Key="Text.Clone.AdditionalParam.Placeholder" xml:space="preserve">其他克隆引數,選填。</x:String>
<x:String x:Key="Text.Clone.LocalName" xml:space="preserve">本地倉庫名 </x:String>
<x:String x:Key="Text.Clone.LocalName.Placeholder" xml:space="preserve">本地倉庫目錄的名字,選填。</x:String>
<x:String x:Key="Text.Clone.ParentFolder" xml:space="preserve">父級目錄 </x:String>
<x:String x:Key="Text.Clone.RemoteURL" xml:space="preserve">遠端倉庫 </x:String>
<x:String x:Key="Text.Close" xml:space="preserve">關閉</x:String>
<x:String x:Key="Text.CommitCM.CherryPick" xml:space="preserve">挑選(cherry-pick)此提交</x:String>
<x:String x:Key="Text.CommitCM.Checkout" xml:space="preserve">檢出此提交</x:String>
<x:String x:Key="Text.CommitCM.CompareWithHead" xml:space="preserve">與當前HEAD比較</x:String>
<x:String x:Key="Text.CommitCM.CompareWithWorktree" xml:space="preserve">與本地工作樹比較</x:String>
<x:String x:Key="Text.CommitCM.CopySHA" xml:space="preserve">複製提交指紋</x:String>
<x:String x:Key="Text.CommitCM.Rebase" xml:space="preserve">變基(rebase)${0}$到此處</x:String>
<x:String x:Key="Text.CommitCM.Reset" xml:space="preserve">重置(reset)${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">另存為補丁 ...</x:String>
<x:String x:Key="Text.CommitCM.Squash" xml:space="preserve">合併此提交到上一個提交</x:String>
<x:String x:Key="Text.CommitDetail.Changes" xml:space="preserve">變更對比</x:String>
<x:String x:Key="Text.CommitDetail.Changes.Search" xml:space="preserve">查詢變更...</x:String>
<x:String x:Key="Text.CommitDetail.Files" xml:space="preserve">檔案列表</x:String>
<x:String x:Key="Text.CommitDetail.Files.LFS" xml:space="preserve">LFS檔案</x:String>
<x:String x:Key="Text.CommitDetail.Files.Submodule" xml:space="preserve">子模組</x:String>
<x:String x:Key="Text.CommitDetail.Info" xml:space="preserve">基本資訊</x:String>
<x:String x:Key="Text.CommitDetail.Info.Author" xml:space="preserve">修改者</x:String>
<x:String x:Key="Text.CommitDetail.Info.Changed" xml:space="preserve">變更列表</x:String>
<x:String x:Key="Text.CommitDetail.Info.Committer" xml:space="preserve">提交者</x:String>
<x:String x:Key="Text.CommitDetail.Info.Message" xml:space="preserve">提交資訊</x:String>
<x:String x:Key="Text.CommitDetail.Info.Parents" xml:space="preserve">父提交</x:String>
<x:String x:Key="Text.CommitDetail.Info.Refs" xml:space="preserve">相關引用</x:String>
<x:String x:Key="Text.CommitDetail.Info.SHA" xml:space="preserve">提交指紋</x:String>
<x:String x:Key="Text.Configure" xml:space="preserve">倉庫配置</x:String>
<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.Proxy" xml:space="preserve">HTTP代理</x:String>
<x:String x:Key="Text.Configure.Proxy.Placeholder" xml:space="preserve">HTTP網路代理</x:String>
<x:String x:Key="Text.Configure.User" xml:space="preserve">使用者名稱</x:String>
<x:String x:Key="Text.Configure.User.Placeholder" xml:space="preserve">應用於本倉庫的使用者名稱</x:String>
<x:String x:Key="Text.Copy" xml:space="preserve">複製</x:String>
<x:String x:Key="Text.CopyPath" xml:space="preserve">複製路徑</x:String>
<x:String x:Key="Text.CopyFileName" xml:space="preserve">複製檔名</x:String>
<x:String x:Key="Text.CreateBranch" xml:space="preserve">新建分支</x:String>
<x:String x:Key="Text.CreateBranch.BasedOn" xml:space="preserve">新分支基於 </x:String>
<x:String x:Key="Text.CreateBranch.Checkout" xml:space="preserve">完成後切換到新分支</x:String>
<x:String x:Key="Text.CreateBranch.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>
<x:String x:Key="Text.CreateBranch.Title" xml:space="preserve">建立本地分支</x:String>
<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.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.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.Cut" xml:space="preserve">剪下</x:String>
<x:String x:Key="Text.DeleteBranch" xml:space="preserve">刪除分支確認</x:String>
<x:String x:Key="Text.DeleteBranch.Branch" xml:space="preserve">分支名 </x:String>
<x:String x:Key="Text.DeleteBranch.IsRemoteTip" xml:space="preserve">您正在刪除遠端上的分支,請務必小心!!!</x:String>
<x:String x:Key="Text.DeleteBranch.WithTrackingRemote" xml:space="preserve">同時刪除遠端分支${0}$</x:String>
<x:String x:Key="Text.DeleteMultiBranch" xml:space="preserve">刪除多個分支</x:String>
<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.Target" xml:space="preserve">目標 </x:String>
<x:String x:Key="Text.DeleteRepositoryNode.TitleForGroup" xml:space="preserve">刪除分組確認</x:String>
<x:String x:Key="Text.DeleteRepositoryNode.TitleForRepository" xml:space="preserve">刪除倉庫確認</x:String>
<x:String x:Key="Text.DeleteSubmodule" xml:space="preserve">刪除子模組確認</x:String>
<x:String x:Key="Text.DeleteSubmodule.Path" xml:space="preserve">子模組路徑 </x:String>
<x:String x:Key="Text.DeleteTag" xml:space="preserve">刪除標籤確認</x:String>
<x:String x:Key="Text.DeleteTag.Tag" xml:space="preserve">標籤名 </x:String>
<x:String x:Key="Text.DeleteTag.WithRemote" xml:space="preserve">同時刪除遠端倉庫中的此標籤</x:String>
<x:String x:Key="Text.Diff.Binary" xml:space="preserve">二進位制檔案</x:String>
<x:String x:Key="Text.Diff.Binary.New" xml:space="preserve">當前大小</x:String>
<x:String x:Key="Text.Diff.Binary.Old" xml:space="preserve">原始大小</x:String>
<x:String x:Key="Text.Diff.Copy" xml:space="preserve">複製</x:String>
<x:String x:Key="Text.Diff.FileModeChanged" xml:space="preserve">檔案許可權已變化</x:String>
<x:String x:Key="Text.Diff.LFS" xml:space="preserve">LFS物件變更</x:String>
<x:String x:Key="Text.Diff.Next" xml:space="preserve">下一個差異</x:String>
<x:String x:Key="Text.Diff.NoChange" xml:space="preserve">沒有變更或僅有換行符差異</x:String>
<x:String x:Key="Text.Diff.Prev" 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.SyntaxHighlight" xml:space="preserve">語法高亮</x:String>
<x:String x:Key="Text.Diff.ToggleWordWrap" xml:space="preserve">自動換行</x:String>
<x:String x:Key="Text.Diff.UseMerger" xml:space="preserve">使用外部合併工具檢視</x:String>
<x:String x:Key="Text.Diff.VisualLines.Decr" xml:space="preserve">減少可見的行數</x:String>
<x:String x:Key="Text.Diff.VisualLines.Incr" xml:space="preserve">增加可見的行數</x:String>
<x:String x:Key="Text.Diff.Welcome" xml:space="preserve">請選擇需要對比的檔案</x:String>
<x:String x:Key="Text.DiffWithMerger" xml:space="preserve">使用外部比對工具檢視</x:String>
<x:String x:Key="Text.Discard" xml:space="preserve">放棄更改確認</x:String>
<x:String x:Key="Text.Discard.All" xml:space="preserve">所有本地址未提交的修改。</x:String>
<x:String x:Key="Text.Discard.Changes" xml:space="preserve">需要放棄的變更 </x:String>
<x:String x:Key="Text.Discard.Total" xml:space="preserve">總計{0}項選中更改</x:String>
<x:String x:Key="Text.Discard.Warning" xml:space="preserve">本操作不支援回退,請確認後繼續!!!</x:String>
<x:String x:Key="Text.EditRepositoryNode.Bookmark" xml:space="preserve">書籤 </x:String>
<x:String x:Key="Text.EditRepositoryNode.Name" xml:space="preserve">名稱 </x:String>
<x:String x:Key="Text.EditRepositoryNode.Target" xml:space="preserve">目標 </x:String>
<x:String x:Key="Text.EditRepositoryNode.TitleForGroup" xml:space="preserve">編輯分組</x:String>
<x:String x:Key="Text.EditRepositoryNode.TitleForRepository" xml:space="preserve">編輯倉庫</x:String>
<x:String x:Key="Text.FastForwardWithoutCheck" xml:space="preserve">快進(fast-forward無需checkout)</x:String>
<x:String x:Key="Text.Fetch" xml:space="preserve">拉取(fetch)</x:String>
<x:String x:Key="Text.Fetch.AllRemotes" xml:space="preserve">拉取所有的遠端倉庫</x:String>
<x:String x:Key="Text.Fetch.Prune" xml:space="preserve">自動清理遠端已刪除分支</x:String>
<x:String x:Key="Text.Fetch.Remote" xml:space="preserve">遠端倉庫 </x:String>
<x:String x:Key="Text.Fetch.Title" xml:space="preserve">拉取遠端倉庫內容</x:String>
<x:String x:Key="Text.FileCM.AssumeUnchanged" xml:space="preserve">不跟蹤此檔案的更改</x:String>
<x:String x:Key="Text.FileCM.Discard" xml:space="preserve">放棄更改...</x:String>
<x:String x:Key="Text.FileCM.DiscardMulti" xml:space="preserve">放棄 {0} 個檔案的更改...</x:String>
<x:String x:Key="Text.FileCM.DiscardSelectedLines" xml:space="preserve">放棄選中的更改</x:String>
<x:String x:Key="Text.FileCM.OpenWithExternalMerger" xml:space="preserve">使用外部合併工具開啟</x:String>
<x:String x:Key="Text.FileCM.SaveAsPatch" xml:space="preserve">另存為補丁...</x:String>
<x:String x:Key="Text.FileCM.Stage" xml:space="preserve">暫存(add)...</x:String>
<x:String x:Key="Text.FileCM.StageMulti" xml:space="preserve">暫存(add){0} 個檔案...</x:String>
<x:String x:Key="Text.FileCM.StageSelectedLines" xml:space="preserve">暫存選中的更改</x:String>
<x:String x:Key="Text.FileCM.Stash" xml:space="preserve">儲藏(stash)...</x:String>
<x:String x:Key="Text.FileCM.StashMulti" xml:space="preserve">儲藏(stash)選中的 {0} 個檔案...</x:String>
<x:String x:Key="Text.FileCM.Unstage" xml:space="preserve">從暫存中移除</x:String>
<x:String x:Key="Text.FileCM.UnstageMulti" xml:space="preserve">從暫存中移除 {0} 個檔案</x:String>
<x:String x:Key="Text.FileCM.UnstageSelectedLines" xml:space="preserve">從暫存中移除選中的更改</x:String>
<x:String x:Key="Text.FileCM.UseTheirs" xml:space="preserve">使用 THEIRS (checkout --theirs)</x:String>
<x:String x:Key="Text.FileCM.UseMine" xml:space="preserve">使用 MINE (checkout --ours)</x:String>
<x:String x:Key="Text.FileHistory" xml:space="preserve">檔案歷史</x:String>
<x:String x:Key="Text.Filter" xml:space="preserve">過濾</x:String>
<x:String x:Key="Text.GitFlow" xml:space="preserve">GIT工作流</x:String>
<x:String x:Key="Text.GitFlow.DevelopBranch" xml:space="preserve">開發分支 </x:String>
<x:String x:Key="Text.GitFlow.Feature" xml:space="preserve">特性分支 </x:String>
<x:String x:Key="Text.GitFlow.FeaturePrefix" xml:space="preserve">特性分支名字首 </x:String>
<x:String x:Key="Text.GitFlow.FinishFeature" xml:space="preserve">結束特性分支</x:String>
<x:String x:Key="Text.GitFlow.FinishHotfix" xml:space="preserve">結束脩復分支</x:String>
<x:String x:Key="Text.GitFlow.FinishRelease" xml:space="preserve">結束版本分支</x:String>
<x:String x:Key="Text.GitFlow.FinishTarget" xml:space="preserve">目標分支 </x:String>
<x:String x:Key="Text.GitFlow.Hotfix" xml:space="preserve">修復分支 </x:String>
<x:String x:Key="Text.GitFlow.HotfixPrefix" xml:space="preserve">修復分支名字首 </x:String>
<x:String x:Key="Text.GitFlow.Init" xml:space="preserve">初始化GIT工作流</x:String>
<x:String x:Key="Text.GitFlow.KeepBranchAfterFinish" xml:space="preserve">保留分支</x:String>
<x:String x:Key="Text.GitFlow.ProductionBranch" xml:space="preserve">釋出分支 </x:String>
<x:String x:Key="Text.GitFlow.Release" xml:space="preserve">版本分支 </x:String>
<x:String x:Key="Text.GitFlow.ReleasePrefix" xml:space="preserve">版本分支名字首 </x:String>
<x:String x:Key="Text.GitFlow.StartFeature" xml:space="preserve">開始特性分支...</x:String>
<x:String x:Key="Text.GitFlow.StartFeatureTitle" xml:space="preserve">開始特性分支</x:String>
<x:String x:Key="Text.GitFlow.StartHotfix" xml:space="preserve">開始修復分支...</x:String>
<x:String x:Key="Text.GitFlow.StartHotfixTitle" xml:space="preserve">開始修復分支</x:String>
<x:String x:Key="Text.GitFlow.StartPlaceholder" xml:space="preserve">輸入分支名</x:String>
<x:String x:Key="Text.GitFlow.StartRelease" xml:space="preserve">開始版本分支...</x:String>
<x:String x:Key="Text.GitFlow.StartReleaseTitle" xml:space="preserve">開始版本分支</x:String>
<x:String x:Key="Text.GitFlow.TagPrefix" xml:space="preserve">版本標籤字首 </x:String>
<x:String x:Key="Text.Histories" xml:space="preserve">歷史記錄</x:String>
<x:String x:Key="Text.Histories.DisplayMode" xml:space="preserve">切換橫向/縱向顯示</x:String>
<x:String x:Key="Text.Histories.GraphMode" xml:space="preserve">切換曲線/折線顯示</x:String>
<x:String x:Key="Text.Histories.Search" xml:space="preserve">查詢提交指紋、資訊、作者。回車鍵開始ESC鍵取消</x:String>
<x:String x:Key="Text.Histories.SearchClear" xml:space="preserve">清空</x:String>
<x:String x:Key="Text.Histories.Selected" xml:space="preserve">已選中 {0} 項提交</x:String>
<x:String x:Key="Text.Hotkeys" xml:space="preserve">快捷鍵參考</x:String>
<x:String x:Key="Text.Hotkeys.Global" xml:space="preserve">全域性快捷鍵</x:String>
<x:String x:Key="Text.Hotkeys.Global.CancelPopup" xml:space="preserve">取消彈出面板</x:String>
<x:String x:Key="Text.Hotkeys.Global.CloseTab" xml:space="preserve">關閉當前頁面</x:String>
<x:String x:Key="Text.Hotkeys.Global.GotoPrevTab" xml:space="preserve">切換到上一個頁面</x:String>
<x:String x:Key="Text.Hotkeys.Global.GotoNextTab" xml:space="preserve">切換到下一個頁面</x:String>
<x:String x:Key="Text.Hotkeys.Global.NewTab" xml:space="preserve">新建頁面</x:String>
<x:String x:Key="Text.Hotkeys.Global.OpenPreference" xml:space="preserve">開啟偏好設定面板</x:String>
<x:String x:Key="Text.Hotkeys.Repo" xml:space="preserve">倉庫頁面快捷鍵</x:String>
<x:String x:Key="Text.Hotkeys.Repo.Refresh" xml:space="preserve">重新載入倉庫狀態</x:String>
<x:String x:Key="Text.Hotkeys.Repo.StageOrUnstageSelected" xml:space="preserve">將選中的變更暫存或從暫存列表中移除</x:String>
<x:String x:Key="Text.Hotkeys.Repo.OpenSearchCommits" xml:space="preserve">開啟歷史搜尋</x:String>
<x:String x:Key="Text.Hotkeys.Repo.ViewChanges" xml:space="preserve">顯示本地更改</x:String>
<x:String x:Key="Text.Hotkeys.Repo.ViewHistories" xml:space="preserve">顯示歷史記錄</x:String>
<x:String x:Key="Text.Hotkeys.Repo.ViewStashes" xml:space="preserve">顯示儲藏列表</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor" xml:space="preserve">文字編輯器</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.CloseSearch" xml:space="preserve">關閉搜尋</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoNextMatch" xml:space="preserve">定位到下一個匹配搜尋的位置</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoPrevMatch" xml:space="preserve">定位到上一個匹配搜尋的位置</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.Search" xml:space="preserve">開啟搜尋</x:String>
<x:String x:Key="Text.Init" xml:space="preserve">初始化新倉庫</x:String>
<x:String x:Key="Text.Init.Path" xml:space="preserve">路徑 </x:String>
<x:String x:Key="Text.Init.Tip" xml:space="preserve">選擇目錄不是有效的Git倉庫。是否需要在此目錄執行`git init`操作?</x:String>
<x:String x:Key="Text.InProgress.CherryPick" xml:space="preserve">挑選Cherry-Pick操作進行中。點選【終止】回滾到操作前的狀態。</x:String>
<x:String x:Key="Text.InProgress.Merge" xml:space="preserve">合併操作進行中。點選【終止】回滾到操作前的狀態。</x:String>
<x:String x:Key="Text.InProgress.Rebase" xml:space="preserve">變基Rebase操作進行中。點選【終止】回滾到操作前的狀態。</x:String>
<x:String x:Key="Text.InProgress.Revert" xml:space="preserve">回滾提交操作進行中。點選【終止】回滾到操作前的狀態。</x:String>
<x:String x:Key="Text.Launcher" xml:space="preserve">Source Git</x:String>
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">出錯了</x:String>
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">系統提示</x:String>
<x:String x:Key="Text.Launcher.Menu" xml:space="preserve">主選單</x:String>
<x:String x:Key="Text.Merge" xml:space="preserve">合併分支</x:String>
<x:String x:Key="Text.Merge.Into" xml:space="preserve">目標分支 </x:String>
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">合併方式 </x:String>
<x:String x:Key="Text.Merge.Source" xml:space="preserve">合併分支 </x:String>
<x:String x:Key="Text.Name" xml:space="preserve">名稱 </x:String>
<x:String x:Key="Text.NotConfigured" xml:space="preserve">GIT尚未配置。請開啟【偏好設定】配置GIT路徑。</x:String>
<x:String x:Key="Text.Notice" xml:space="preserve">系統提示</x:String>
<x:String x:Key="Text.OpenFolder" xml:space="preserve">選擇資料夾</x:String>
<x:String x:Key="Text.OpenWith" xml:space="preserve">開啟檔案...</x:String>
<x:String x:Key="Text.Optional" xml:space="preserve">選填。</x:String>
<x:String x:Key="Text.PageTabBar.New" xml:space="preserve">新建空白頁</x:String>
<x:String x:Key="Text.PageTabBar.Tab.Bookmark" xml:space="preserve">設定書籤</x:String>
<x:String x:Key="Text.PageTabBar.Tab.Close" xml:space="preserve">關閉標籤頁</x:String>
<x:String x:Key="Text.PageTabBar.Tab.CloseOther" xml:space="preserve">關閉其他標籤頁</x:String>
<x:String x:Key="Text.PageTabBar.Tab.CloseRight" xml:space="preserve">關閉右側標籤頁</x:String>
<x:String x:Key="Text.PageTabBar.Tab.CopyPath" xml:space="preserve">複製倉庫路徑</x:String>
<x:String x:Key="Text.PageTabBar.Welcome.Title" xml:space="preserve">新標籤頁</x:String>
<x:String x:Key="Text.Paste" xml:space="preserve">貼上</x:String>
<x:String x:Key="Text.Preference" xml:space="preserve">偏好設定</x:String>
<x:String x:Key="Text.Preference.Appearance" xml:space="preserve">外觀配置</x:String>
<x:String x:Key="Text.Preference.Appearance.ColorOverrides" xml:space="preserve">自訂配色檔</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFont" xml:space="preserve">預設字型</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFontSize" xml:space="preserve">預設字型大小</x:String>
<x:String x:Key="Text.Preference.Appearance.MonospaceFont" xml:space="preserve">等寬字型</x:String>
<x:String x:Key="Text.Preference.Appearance.Theme" xml:space="preserve">主題</x:String>
<x:String x:Key="Text.Preference.General" xml:space="preserve">通用配置</x:String>
<x:String x:Key="Text.Preference.General.AvatarServer" xml:space="preserve">頭像服務</x:String>
<x:String x:Key="Text.Preference.General.Check4UpdatesOnStartup" xml:space="preserve">啟動時檢測軟體更新</x:String>
<x:String x:Key="Text.Preference.General.Locale" xml:space="preserve">顯示語言</x:String>
<x:String x:Key="Text.Preference.General.MaxHistoryCommits" xml:space="preserve">最大歷史提交數</x:String>
<x:String x:Key="Text.Preference.General.RestoreTabs" xml:space="preserve">啟動時恢復上次開啟的倉庫</x:String>
<x:String x:Key="Text.Preference.General.UseFixedTabWidth" xml:space="preserve">使用固定寬度的標題欄標籤</x:String>
<x:String x:Key="Text.Preference.Git" xml:space="preserve">GIT配置</x:String>
<x:String x:Key="Text.Preference.Git.AutoFetch" xml:space="preserve">啟用定時自動拉取遠端更新</x:String>
<x:String x:Key="Text.Preference.Git.AutoFetchInterval" xml:space="preserve">自動拉取間隔</x:String>
<x:String x:Key="Text.Preference.Git.AutoFetchIntervalSuffix" xml:space="preserve">分鐘</x:String>
<x:String x:Key="Text.Preference.Git.CRLF" xml:space="preserve">自動換行轉換</x:String>
<x:String x:Key="Text.Preference.Git.DefaultCloneDir" xml:space="preserve">預設克隆路徑</x:String>
<x:String x:Key="Text.Preference.Git.Email" xml:space="preserve">郵箱</x:String>
<x:String x:Key="Text.Preference.Git.Email.Placeholder" xml:space="preserve">預設GIT使用者郵箱</x:String>
<x:String x:Key="Text.Preference.Git.Path" xml:space="preserve">安裝路徑</x:String>
<x:String x:Key="Text.Preference.Git.Shell" xml:space="preserve">終端Shell</x:String>
<x:String x:Key="Text.Preference.Git.User" xml:space="preserve">使用者名稱</x:String>
<x:String x:Key="Text.Preference.Git.User.Placeholder" xml:space="preserve">預設GIT使用者名稱</x:String>
<x:String x:Key="Text.Preference.Git.Version" xml:space="preserve">Git 版本</x:String>
<x:String x:Key="Text.Preference.Git.Invalid" xml:space="preserve">本軟體要求GIT最低版本為2.23.0</x:String>
<x:String x:Key="Text.Preference.GPG" xml:space="preserve">GPG簽名</x:String>
<x:String x:Key="Text.Preference.GPG.CommitEnabled" xml:space="preserve">啟用提交簽名</x:String>
<x:String x:Key="Text.Preference.GPG.TagEnabled" xml:space="preserve">啟用標籤簽名</x:String>
<x:String x:Key="Text.Preference.GPG.Format" xml:space="preserve">GPG簽名格式</x:String>
<x:String x:Key="Text.Preference.GPG.Path" xml:space="preserve">可執行檔案位置</x:String>
<x:String x:Key="Text.Preference.GPG.Path.Placeholder" xml:space="preserve">gpg.exe所在路徑</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey" xml:space="preserve">使用者簽名KEY</x:String>
<x:String x:Key="Text.Preference.GPG.UserKey.Placeholder" xml:space="preserve">輸入簽名提交所使用的KEY</x:String>
<x:String x:Key="Text.Preference.Merger" xml:space="preserve">外部合併工具</x:String>
<x:String x:Key="Text.Preference.Merger.CustomDiffCmd" xml:space="preserve">對比模式啟動引數</x:String>
<x:String x:Key="Text.Preference.Merger.CustomMergeCmd" xml:space="preserve">合併模式啟動引數</x:String>
<x:String x:Key="Text.Preference.Merger.Path" xml:space="preserve">安裝路徑</x:String>
<x:String x:Key="Text.Preference.Merger.Path.Placeholder" xml:space="preserve">填寫工具可執行檔案所在位置</x:String>
<x:String x:Key="Text.Preference.Merger.Type" xml:space="preserve">工具</x:String>
<x:String x:Key="Text.Pull" xml:space="preserve">拉回(pull)</x:String>
<x:String x:Key="Text.Pull.Branch" xml:space="preserve">拉取分支 </x:String>
<x:String x:Key="Text.Pull.Into" xml:space="preserve">本地分支 </x:String>
<x:String x:Key="Text.Pull.LocalChanges" xml:space="preserve">未提交更改 </x:String>
<x:String x:Key="Text.Pull.LocalChanges.Discard" xml:space="preserve">丟棄更改</x:String>
<x:String x:Key="Text.Pull.LocalChanges.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.Remote" xml:space="preserve">遠端 </x:String>
<x:String x:Key="Text.Pull.Title" xml:space="preserve">拉回(拉取併合並)</x:String>
<x:String x:Key="Text.Pull.UseRebase" xml:space="preserve">使用變基方式合併分支</x:String>
<x:String x:Key="Text.Push" xml:space="preserve">推送(push)</x:String>
<x:String x:Key="Text.Push.Force" xml:space="preserve">啟用強制推送</x:String>
<x:String x:Key="Text.Push.Local" xml:space="preserve">本地分支 </x:String>
<x:String x:Key="Text.Push.Remote" xml:space="preserve">遠端倉庫 </x:String>
<x:String x:Key="Text.Push.Title" xml:space="preserve">推送到遠端倉庫</x:String>
<x:String x:Key="Text.Push.To" xml:space="preserve">遠端分支 </x:String>
<x:String x:Key="Text.Push.Tracking" xml:space="preserve">跟蹤遠端分支</x:String>
<x:String x:Key="Text.Push.WithAllTags" xml:space="preserve">同時推送標籤</x:String>
<x:String x:Key="Text.PushTag" xml:space="preserve">推送標籤到遠端倉庫</x:String>
<x:String x:Key="Text.PushTag.PushAllRemotes" xml:space="preserve">推送到所有遠端倉庫</x:String>
<x:String x:Key="Text.PushTag.Remote" xml:space="preserve">遠端倉庫 </x:String>
<x:String x:Key="Text.PushTag.Tag" xml:space="preserve">標籤 </x:String>
<x:String x:Key="Text.Quit" xml:space="preserve">退出</x:String>
<x:String x:Key="Text.Rebase" xml:space="preserve">變基(rebase)操作</x:String>
<x:String x:Key="Text.Rebase.AutoStash" xml:space="preserve">自動儲藏並恢復本地變更</x:String>
<x:String x:Key="Text.Rebase.On" xml:space="preserve">目標提交 </x:String>
<x:String x:Key="Text.Rebase.Target" xml:space="preserve">分支 </x:String>
<x:String x:Key="Text.RefetchAvatar" xml:space="preserve">重新載入</x:String>
<x:String x:Key="Text.Remote.AddTitle" xml:space="preserve">新增遠端倉庫</x:String>
<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.Placeholder" xml:space="preserve">遠端倉庫的地址</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>
<x:String x:Key="Text.RemoteCM.Edit" xml:space="preserve">編輯 ...</x:String>
<x:String x:Key="Text.RemoteCM.Fetch" xml:space="preserve">拉取(fetch)更新 ...</x:String>
<x:String x:Key="Text.RemoteCM.Prune" xml:space="preserve">清理遠端已刪除分支</x:String>
<x:String x:Key="Text.RemoteCM.Prune.Target" xml:space="preserve">目標 </x:String>
<x:String x:Key="Text.RenameBranch" xml:space="preserve">分支重新命名</x:String>
<x:String x:Key="Text.RenameBranch.Name" xml:space="preserve">新的名稱 </x:String>
<x:String x:Key="Text.RenameBranch.Name.Placeholder" xml:space="preserve">新的分支名不能與現有分支名相同</x:String>
<x:String x:Key="Text.RenameBranch.Target" xml:space="preserve">分支 </x:String>
<x:String x:Key="Text.Repository.Abort" xml:space="preserve">終止合併</x:String>
<x:String x:Key="Text.Repository.Clean" xml:space="preserve">清理本倉庫(GC)</x:String>
<x:String x:Key="Text.Repository.CleanTips" xml:space="preserve">本操作將執行`gc`對於啟用LFS的倉庫也會執行`lfs prune`。</x:String>
<x:String x:Key="Text.Repository.Configure" xml:space="preserve">配置本倉庫</x:String>
<x:String x:Key="Text.Repository.Continue" xml:space="preserve">下一步</x:String>
<x:String x:Key="Text.Repository.Explore" xml:space="preserve">在檔案瀏覽器中開啟</x:String>
<x:String x:Key="Text.Repository.FilterBranchTip" xml:space="preserve">過濾顯示分支</x:String>
<x:String x:Key="Text.Repository.LocalBranches" xml:space="preserve">本地分支</x:String>
<x:String x:Key="Text.Repository.NavigateToCurrentHead" xml:space="preserve">定位HEAD</x:String>
<x:String x:Key="Text.Repository.NewBranch" xml:space="preserve">新建分支</x:String>
<x:String x:Key="Text.Repository.OpenIn" xml:space="preserve">在 {0} 中開啟</x:String>
<x:String x:Key="Text.Repository.OpenWithExternalTools" xml:space="preserve">使用外部工具開啟</x:String>
<x:String x:Key="Text.Repository.Refresh" xml:space="preserve">重新載入</x:String>
<x:String x:Key="Text.Repository.Remotes" xml:space="preserve">遠端列表</x:String>
<x:String x:Key="Text.Repository.Remotes.Add" xml:space="preserve">新增遠端</x:String>
<x:String x:Key="Text.Repository.Resolve" xml:space="preserve">解決衝突</x:String>
<x:String x:Key="Text.Repository.Search" xml:space="preserve">查詢提交</x:String>
<x:String x:Key="Text.Repository.Search.By" xml:space="preserve">查詢方式</x:String>
<x:String x:Key="Text.Repository.Search.ByBaseInfo" xml:space="preserve">摘要</x:String>
<x:String x:Key="Text.Repository.Search.ByFile" xml:space="preserve">檔案</x:String>
<x:String x:Key="Text.Repository.Statistics" xml:space="preserve">提交統計</x:String>
<x:String x:Key="Text.Repository.Submodules" xml:space="preserve">子模組列表</x:String>
<x:String x:Key="Text.Repository.Submodules.Add" xml:space="preserve">新增子模組</x:String>
<x:String x:Key="Text.Repository.Submodules.Update" xml:space="preserve">更新子模組</x:String>
<x:String x:Key="Text.Repository.Tags" xml:space="preserve">標籤列表</x:String>
<x:String x:Key="Text.Repository.Tags.Add" xml:space="preserve">新建標籤</x:String>
<x:String x:Key="Text.Repository.Terminal" xml:space="preserve">在終端中開啟</x:String>
<x:String x:Key="Text.Repository.Workspace" xml:space="preserve">工作區</x:String>
<x:String x:Key="Text.RepositoryURL" xml:space="preserve">遠端倉庫地址</x:String>
<x:String x:Key="Text.Reset" xml:space="preserve">重置(reset)當前分支到指定版本</x:String>
<x:String x:Key="Text.Reset.Mode" xml:space="preserve">重置模式 </x:String>
<x:String x:Key="Text.Reset.MoveTo" xml:space="preserve">提交 </x:String>
<x:String x:Key="Text.Reset.Target" xml:space="preserve">當前分支 </x:String>
<x:String x:Key="Text.RevealFile" xml:space="preserve">在檔案瀏覽器中檢視</x:String>
<x:String x:Key="Text.Revert" xml:space="preserve">回滾操作確認</x:String>
<x:String x:Key="Text.Revert.Commit" xml:space="preserve">目標提交 </x:String>
<x:String x:Key="Text.Revert.CommitChanges" xml:space="preserve">回滾後提交更改</x:String>
<x:String x:Key="Text.Reword" xml:space="preserve">編輯提交資訊</x:String>
<x:String x:Key="Text.Reword.Message" xml:space="preserve">提交資訊:</x:String>
<x:String x:Key="Text.Reword.On" xml:space="preserve">提交:</x:String>
<x:String x:Key="Text.Running" xml:space="preserve">執行操作中,請耐心等待...</x:String>
<x:String x:Key="Text.Save" xml:space="preserve">保 存</x:String>
<x:String x:Key="Text.SaveAs" xml:space="preserve">另存為...</x:String>
<x:String x:Key="Text.SaveAsPatchSuccess" xml:space="preserve">補丁已成功儲存!</x:String>
<x:String x:Key="Text.SelfUpdate" xml:space="preserve">檢測更新...</x:String>
<x:String x:Key="Text.SelfUpdate.Available" xml:space="preserve">檢測到軟體有版本更新: </x:String>
<x:String x:Key="Text.SelfUpdate.Error" xml:space="preserve">獲取最新版本資訊失敗!</x:String>
<x:String x:Key="Text.SelfUpdate.GotoDownload" xml:space="preserve">下 載</x:String>
<x:String x:Key="Text.SelfUpdate.IgnoreThisVersion" xml:space="preserve">忽略此版本</x:String>
<x:String x:Key="Text.SelfUpdate.Title" xml:space="preserve">軟體更新</x:String>
<x:String x:Key="Text.SelfUpdate.UpToDate" xml:space="preserve">當前已是最新版本。</x:String>
<x:String x:Key="Text.Squash" xml:space="preserve">合併HEAD到上一個提交</x:String>
<x:String x:Key="Text.Squash.Head" xml:space="preserve">當前提交 :</x:String>
<x:String x:Key="Text.Squash.Message" xml:space="preserve">修改提交資訊:</x:String>
<x:String x:Key="Text.Squash.To" xml:space="preserve">合併到 :</x:String>
<x:String x:Key="Text.SSHKey" xml:space="preserve">SSH金鑰 </x:String>
<x:String x:Key="Text.SSHKey.Placeholder" xml:space="preserve">SSH金鑰檔案</x:String>
<x:String x:Key="Text.Start" xml:space="preserve">開 始</x:String>
<x:String x:Key="Text.Stash" xml:space="preserve">儲藏(stash)</x:String>
<x:String x:Key="Text.Stash.IncludeUntracked" xml:space="preserve">包含未跟蹤的檔案</x:String>
<x:String x:Key="Text.Stash.Message" xml:space="preserve">資訊 </x:String>
<x:String x:Key="Text.Stash.Message.Placeholder" xml:space="preserve">選填,用於命名此儲藏</x:String>
<x:String x:Key="Text.Stash.Title" xml:space="preserve">儲藏本地變更</x:String>
<x:String x:Key="Text.StashCM.Apply" xml:space="preserve">應用(apply)</x:String>
<x:String x:Key="Text.StashCM.Drop" xml:space="preserve">刪除(drop)</x:String>
<x:String x:Key="Text.StashCM.Pop" xml:space="preserve">應用並刪除(pop)</x:String>
<x:String x:Key="Text.StashDropConfirm" xml:space="preserve">丟棄儲藏確認</x:String>
<x:String x:Key="Text.StashDropConfirm.Label" xml:space="preserve">丟棄儲藏 </x:String>
<x:String x:Key="Text.Stashes" xml:space="preserve">儲藏列表</x:String>
<x:String x:Key="Text.Stashes.Changes" xml:space="preserve">檢視變更</x:String>
<x:String x:Key="Text.Stashes.Stashes" xml:space="preserve">儲藏列表</x:String>
<x:String x:Key="Text.Statistics" xml:space="preserve">提交統計</x:String>
<x:String x:Key="Text.Statistics.CommitAmount" xml:space="preserve">提交次數</x:String>
<x:String x:Key="Text.Statistics.Committer" xml:space="preserve">提交者</x:String>
<x:String x:Key="Text.Statistics.ThisMonth" xml:space="preserve">本月</x:String>
<x:String x:Key="Text.Statistics.ThisWeek" xml:space="preserve">本週</x:String>
<x:String x:Key="Text.Statistics.ThisYear" xml:space="preserve">本年</x:String>
<x:String x:Key="Text.Statistics.TotalCommits" xml:space="preserve">提交次數: </x:String>
<x:String x:Key="Text.Statistics.TotalCommitters" xml:space="preserve">提交者: </x:String>
<x:String x:Key="Text.Submodule" xml:space="preserve">子模組</x:String>
<x:String x:Key="Text.Submodule.Add" xml:space="preserve">新增子模組</x:String>
<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.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>
<x:String x:Key="Text.TagCM.Delete" xml:space="preserve">刪除${0}$</x:String>
<x:String x:Key="Text.TagCM.Push" xml:space="preserve">推送${0}$</x:String>
<x:String x:Key="Text.URL" xml:space="preserve">倉庫地址 </x:String>
<x:String x:Key="Text.UpdateSubmodules" xml:space="preserve">更新子模組</x:String>
<x:String x:Key="Text.UpdateSubmodules.Tip" xml:space="preserve">本操作將執行 `submodule update` 。</x:String>
<x:String x:Key="Text.Warn" xml:space="preserve">警告</x:String>
<x:String x:Key="Text.Welcome.AddRootFolder" xml:space="preserve">新建分組</x:String>
<x:String x:Key="Text.Welcome.AddSubFolder" xml:space="preserve">新建子分組</x:String>
<x:String x:Key="Text.Welcome.Clone" xml:space="preserve">克隆遠端倉庫</x:String>
<x:String x:Key="Text.Welcome.Delete" xml:space="preserve">刪除</x:String>
<x:String x:Key="Text.Welcome.DragDropTip" xml:space="preserve">支援拖放目錄新增。支援自定義分組。</x:String>
<x:String x:Key="Text.Welcome.Edit" xml:space="preserve">編輯</x:String>
<x:String x:Key="Text.Welcome.OpenOrInit" xml:space="preserve">開啟本地倉庫</x:String>
<x:String x:Key="Text.Welcome.OpenTerminal" xml:space="preserve">開啟終端</x:String>
<x:String x:Key="Text.Welcome.Search" xml:space="preserve">快速查詢倉庫...</x:String>
<x:String x:Key="Text.Welcome.Sort" xml:space="preserve">排序</x:String>
<x:String x:Key="Text.WorkingCopy" xml:space="preserve">本地更改</x:String>
<x:String x:Key="Text.WorkingCopy.Amend" xml:space="preserve">修補(--amend)</x:String>
<x:String x:Key="Text.WorkingCopy.CanStageTip" xml:space="preserve">現在您已可將其加入暫存區中</x:String>
<x:String x:Key="Text.WorkingCopy.Commit" xml:space="preserve">提交</x:String>
<x:String x:Key="Text.WorkingCopy.CommitAndPush" xml:space="preserve">提交併推送</x:String>
<x:String x:Key="Text.WorkingCopy.CommitMessageTip" xml:space="preserve">填寫提交資訊</x:String>
<x:String x:Key="Text.WorkingCopy.CommitTip" xml:space="preserve">CTRL + Enter</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts" xml:space="preserve">檢測到衝突</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.Resolved" xml:space="preserve">檔案衝突已解決</x:String>
<x:String x:Key="Text.WorkingCopy.HasCommitHistories" xml:space="preserve">最近輸入的提交資訊</x:String>
<x:String x:Key="Text.WorkingCopy.IncludeUntracked" xml:space="preserve">顯示未跟蹤檔案</x:String>
<x:String x:Key="Text.WorkingCopy.MessageHistories" xml:space="preserve">歷史提交資訊</x:String>
<x:String x:Key="Text.WorkingCopy.NoCommitHistories" xml:space="preserve">沒有提交資訊記錄</x:String>
<x:String x:Key="Text.WorkingCopy.Staged" xml:space="preserve">已暫存</x:String>
<x:String x:Key="Text.WorkingCopy.Staged.Unstage" xml:space="preserve">從暫存區移除選中</x:String>
<x:String x:Key="Text.WorkingCopy.Staged.UnstageAll" xml:space="preserve">從暫存區移除所有</x:String>
<x:String x:Key="Text.WorkingCopy.Unstaged" xml:space="preserve">未暫存</x:String>
<x:String x:Key="Text.WorkingCopy.Unstaged.Stage" xml:space="preserve">暫存選中</x:String>
<x:String x:Key="Text.WorkingCopy.Unstaged.StageAll" xml:space="preserve">暫存所有</x:String>
<x:String x:Key="Text.WorkingCopy.Unstaged.ViewAssumeUnchaged" xml:space="preserve">檢視忽略變更檔案</x:String>
<x:String x:Key="Text.WorkingCopy.ResolveTip" xml:space="preserve">請選中衝突檔案,開啟右鍵選單,選擇合適的解決方式</x:String>
<x:String x:Key="Text.Worktree" xml:space="preserve">本地工作樹</x:String>
</ResourceDictionary>

View file

@ -1045,31 +1045,32 @@
<ControlTemplate>
<StackPanel>
<Border Name="PART_LayoutRoot"
Classes="TreeViewItemLayoutRoot"
Focusable="True"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Background="Transparent"
BorderThickness="0"
CornerRadius="0"
MinHeight="{TemplateBinding MinHeight}"
TemplatedControl.IsTemplateFocusTarget="True">
<Grid Name="PART_Header" ColumnDefinitions="16,*" Margin="{TemplateBinding Level, Mode=OneWay, Converter={StaticResource TreeViewItemLeftMarginConverter}}">
<Panel Name="PART_ExpandCollapseChevronContainer">
<ToggleButton Name="PART_ExpandCollapseChevron"
Classes="tree_expander"
Focusable="False"
HorizontalAlignment="Center"
IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}" />
</Panel>
<ContentPresenter Name="PART_HeaderPresenter"
Grid.Column="1"
<Grid>
<Border Name="PART_Background" CornerRadius="{TemplateBinding CornerRadius}" Background="Transparent"/>
<Grid Name="PART_Header" ColumnDefinitions="16,*" Margin="{TemplateBinding Level, Mode=OneWay, Converter={StaticResource TreeViewItemLeftMarginConverter}}">
<Panel Name="PART_ExpandCollapseChevronContainer">
<ToggleButton Name="PART_ExpandCollapseChevron"
Classes="tree_expander"
Focusable="False"
Background="Transparent"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Margin="{TemplateBinding Padding}" />
HorizontalAlignment="Center"
IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}" />
</Panel>
<ContentPresenter Name="PART_HeaderPresenter"
Grid.Column="1"
Focusable="False"
Background="Transparent"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Margin="{TemplateBinding Padding}" />
</Grid>
</Grid>
</Border>
<ItemsPresenter Name="PART_ItemsPresenter"
@ -1080,11 +1081,22 @@
</Setter>
<Style Selector="^ /template/ Border#PART_LayoutRoot:pointerover">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="^ /template/ Border#PART_LayoutRoot:pointerover Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.AccentHovered}" />
</Style>
<Style Selector="^:selected /template/ Border#PART_LayoutRoot">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="^:selected /template/ Border#PART_LayoutRoot Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".4"/>
</Style>
<Style Selector="^:selected /template/ Border#PART_LayoutRoot:pointerover Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
</Style>
<Style Selector="TreeViewItem[IsExpanded=True] Path.folder_icon">

View file

@ -25,6 +25,11 @@
<Color x:Key="Color.FG1">#FF1F1F1F</Color>
<Color x:Key="Color.FG2">#FF6F6F6F</Color>
<Color x:Key="Color.FG3">#FFFFFFFF</Color>
<Color x:Key="Color.TextDiffView.LineBG1.EMPTY">#3C000000</Color>
<Color x:Key="Color.TextDiffView.LineBG1.ADD">#3C00FF00</Color>
<Color x:Key="Color.TextDiffView.LineBG1.DELETED">#3CFF0000</Color>
<Color x:Key="Color.TextDiffView.LineBG2.ADD">#5A00FF00</Color>
<Color x:Key="Color.TextDiffView.LineBG2.DELETED">#50FF0000</Color>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
@ -35,7 +40,7 @@
<Color x:Key="Color.TitleBar">#FF1F1F1F</Color>
<Color x:Key="Color.ToolBar">#FF2C2C2C</Color>
<Color x:Key="Color.Popup">#FF2B2B2B</Color>
<Color x:Key="Color.Contents">#FF181818</Color>
<Color x:Key="Color.Contents">#FF1B1B1B</Color>
<Color x:Key="Color.Badge">#FF8F8F8F</Color>
<Color x:Key="Color.Decorator">#FF505050</Color>
<Color x:Key="Color.DecoratorIcon">#FFF8F8F8</Color>
@ -51,6 +56,11 @@
<Color x:Key="Color.FG1">#FFDDDDDD</Color>
<Color x:Key="Color.FG2">#40F1F1F1</Color>
<Color x:Key="Color.FG3">#FF252525</Color>
<Color x:Key="Color.TextDiffView.LineBG1.EMPTY">#3C000000</Color>
<Color x:Key="Color.TextDiffView.LineBG1.ADD">#3C00FF00</Color>
<Color x:Key="Color.TextDiffView.LineBG1.DELETED">#3CFF0000</Color>
<Color x:Key="Color.TextDiffView.LineBG2.ADD">#5A00FF00</Color>
<Color x:Key="Color.TextDiffView.LineBG2.DELETED">#50FF0000</Color>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
@ -79,4 +89,9 @@
<SolidColorBrush x:Key="Brush.FG3" Color="{DynamicResource Color.FG3}"/>
<SolidColorBrush x:Key="Brush.Accent" Color="{DynamicResource SystemAccentColor}"/>
<SolidColorBrush x:Key="Brush.AccentHovered" Color="{DynamicResource SystemListLowColor}"/>
<SolidColorBrush x:Key="Brush.TextDiffView.LineBG1.EMPTY" Color="{DynamicResource Color.TextDiffView.LineBG1.EMPTY}"/>
<SolidColorBrush x:Key="Brush.TextDiffView.LineBG1.ADD" Color="{DynamicResource Color.TextDiffView.LineBG1.ADD}"/>
<SolidColorBrush x:Key="Brush.TextDiffView.LineBG1.DELETED" Color="{DynamicResource Color.TextDiffView.LineBG1.DELETED}"/>
<SolidColorBrush x:Key="Brush.TextDiffView.LineBG2.ADD" Color="{DynamicResource Color.TextDiffView.LineBG2.ADD}"/>
<SolidColorBrush x:Key="Brush.TextDiffView.LineBG2.DELETED" Color="{DynamicResource Color.TextDiffView.LineBG2.DELETED}"/>
</ResourceDictionary>

View file

@ -79,11 +79,8 @@ namespace SourceGit.ViewModels
public static ValidationResult ValidateSSHKey(string sshkey, ValidationContext ctx)
{
if (ctx.ObjectInstance is AddRemote add && add._useSSH)
if (ctx.ObjectInstance is AddRemote { _useSSH: true } && !string.IsNullOrEmpty(sshkey))
{
if (string.IsNullOrEmpty(sshkey))
return new ValidationResult("SSH private key is required");
if (!File.Exists(sshkey))
return new ValidationResult("Given SSH private key can NOT be found!");
}
@ -102,10 +99,8 @@ namespace SourceGit.ViewModels
if (succ)
{
SetProgressDescription("Fetching from added remote ...");
new Commands.Fetch(_repo.FullPath, _name, true, SetProgressDescription).Exec();
SetProgressDescription("Post processing ...");
new Commands.Config(_repo.FullPath).Set($"remote.{_name}.sshkey", _useSSH ? SSHKey : null);
new Commands.Fetch(_repo.FullPath, _name, true, SetProgressDescription).Exec();
}
CallUIThread(() =>
{

View file

@ -2,7 +2,7 @@
namespace SourceGit.ViewModels
{
public class CheckoutCommit: Popup
public class CheckoutCommit : Popup
{
public Models.Commit Commit
{

View file

@ -4,7 +4,6 @@ using System.IO;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Models.TreeDataGrid;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
@ -37,6 +36,12 @@ namespace SourceGit.ViewModels
}
}
public string FullMessage
{
get => _fullMessage;
private set => SetProperty(ref _fullMessage, value);
}
public List<Models.Change> Changes
{
get => _changes;
@ -76,24 +81,6 @@ namespace SourceGit.ViewModels
}
}
public HierarchicalTreeDataGridSource<Models.FileTreeNode> RevisionFiles
{
get => _revisionFiles;
private set => SetProperty(ref _revisionFiles, value);
}
public string SearchFileFilter
{
get => _searchFileFilter;
set
{
if (SetProperty(ref _searchFileFilter, value))
{
RefreshVisibleFiles();
}
}
}
public object ViewRevisionFileContent
{
get => _viewRevisionFileContent;
@ -117,11 +104,6 @@ namespace SourceGit.ViewModels
_selectedChanges.Clear();
_searchChangeFilter = null;
_diffContext = null;
if (_revisionFilesBackup != null)
_revisionFilesBackup.Clear();
if (_revisionFiles != null)
_revisionFiles.Dispose();
_searchFileFilter = null;
_viewRevisionFileContent = null;
_cancelToken = null;
}
@ -138,9 +120,93 @@ namespace SourceGit.ViewModels
SearchChangeFilter = string.Empty;
}
public void ClearSearchFileFilter()
public List<Models.Object> GetRevisionFilesUnderFolder(string parentFolder)
{
SearchFileFilter = string.Empty;
return new Commands.QueryRevisionObjects(_repo, _commit.SHA, parentFolder).Result();
}
public void ViewRevisionFile(Models.Object file)
{
if (file == null)
{
ViewRevisionFileContent = null;
return;
}
switch (file.Type)
{
case Models.ObjectType.Blob:
Task.Run(() =>
{
var isBinary = new Commands.IsBinary(_repo, _commit.SHA, file.Path).Result();
if (isBinary)
{
var ext = Path.GetExtension(file.Path);
if (IMG_EXTS.Contains(ext))
{
var stream = Commands.QueryFileContent.Run(_repo, _commit.SHA, file.Path);
var bitmap = stream.Length > 0 ? new Bitmap(stream) : null;
Dispatcher.UIThread.Invoke(() =>
{
ViewRevisionFileContent = new Models.RevisionImageFile() { Image = bitmap };
});
}
else
{
var size = new Commands.QueryFileSize(_repo, file.Path, _commit.SHA).Result();
Dispatcher.UIThread.Invoke(() =>
{
ViewRevisionFileContent = new Models.RevisionBinaryFile() { Size = size };
});
}
return;
}
var contentStream = Commands.QueryFileContent.Run(_repo, _commit.SHA, file.Path);
var content = new StreamReader(contentStream).ReadToEnd();
if (content.StartsWith("version https://git-lfs.github.com/spec/", StringComparison.Ordinal))
{
var obj = new Models.RevisionLFSObject() { Object = new Models.LFSObject() };
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
if (lines.Length == 3)
{
foreach (var line in lines)
{
if (line.StartsWith("oid sha256:", StringComparison.Ordinal))
{
obj.Object.Oid = line.Substring(11);
}
else if (line.StartsWith("size ", StringComparison.Ordinal))
{
obj.Object.Size = long.Parse(line.Substring(5));
}
}
Dispatcher.UIThread.Invoke(() =>
{
ViewRevisionFileContent = obj;
});
return;
}
}
Dispatcher.UIThread.Invoke(() =>
{
ViewRevisionFileContent = new Models.RevisionTextFile()
{
FileName = file.Path,
Content = content
};
});
});
break;
case Models.ObjectType.Commit:
ViewRevisionFileContent = new Models.RevisionSubmodule() { SHA = file.SHA };
break;
default:
ViewRevisionFileContent = null;
break;
}
}
public ContextMenu CreateChangeContextMenu(Models.Change change)
@ -316,32 +382,24 @@ namespace SourceGit.ViewModels
private void Refresh()
{
_changes = null;
FullMessage = string.Empty;
VisibleChanges = null;
SelectedChanges = null;
if (_revisionFiles != null)
{
_revisionFiles.Dispose();
_revisionFiles = null;
}
if (_commit == null)
return;
if (_cancelToken != null)
_cancelToken.Requested = true;
_cancelToken = new Commands.Command.CancelToken();
var parent = _commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : _commit.Parents[0];
var cmdChanges = new Commands.CompareRevisions(_repo, parent, _commit.SHA) { Cancel = _cancelToken };
var cmdRevisionFiles = new Commands.QueryRevisionObjects(_repo, _commit.SHA) { Cancel = _cancelToken };
Task.Run(() =>
{
var fullMessage = new Commands.QueryCommitFullMessage(_repo, _commit.SHA).Result();
var parent = _commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : _commit.Parents[0];
var cmdChanges = new Commands.CompareRevisions(_repo, parent, _commit.SHA) { Cancel = _cancelToken };
var changes = cmdChanges.Result();
if (cmdChanges.Cancel.Requested)
return;
var visible = changes;
if (!string.IsNullOrWhiteSpace(_searchChangeFilter))
{
@ -349,39 +407,19 @@ namespace SourceGit.ViewModels
foreach (var c in changes)
{
if (c.Path.Contains(_searchChangeFilter, StringComparison.OrdinalIgnoreCase))
{
visible.Add(c);
}
}
}
Dispatcher.UIThread.Invoke(() =>
if (!cmdChanges.Cancel.Requested)
{
Changes = changes;
VisibleChanges = visible;
});
});
Task.Run(() =>
{
_revisionFilesBackup = cmdRevisionFiles.Result();
if (cmdRevisionFiles.Cancel.Requested)
return;
var visible = _revisionFilesBackup;
var isSearching = !string.IsNullOrWhiteSpace(_searchFileFilter);
if (isSearching)
{
visible = new List<Models.Object>();
foreach (var f in _revisionFilesBackup)
Dispatcher.UIThread.Post(() =>
{
if (f.Path.Contains(_searchFileFilter, StringComparison.OrdinalIgnoreCase))
visible.Add(f);
}
FullMessage = fullMessage;
Changes = changes;
VisibleChanges = visible;
});
}
var tree = Models.FileTreeNode.Build(visible, isSearching || visible.Count <= 100);
Dispatcher.UIThread.Invoke(() => BuildRevisionFilesSource(tree));
});
}
@ -407,140 +445,6 @@ namespace SourceGit.ViewModels
}
}
private void RefreshVisibleFiles()
{
if (_revisionFiles == null)
return;
var visible = _revisionFilesBackup;
var isSearching = !string.IsNullOrWhiteSpace(_searchFileFilter);
if (isSearching)
{
visible = new List<Models.Object>();
foreach (var f in _revisionFilesBackup)
{
if (f.Path.Contains(_searchFileFilter, StringComparison.OrdinalIgnoreCase))
visible.Add(f);
}
}
BuildRevisionFilesSource(Models.FileTreeNode.Build(visible, isSearching || visible.Count < 100));
}
private void RefreshViewRevisionFile(Models.Object file)
{
if (file == null)
{
ViewRevisionFileContent = null;
return;
}
switch (file.Type)
{
case Models.ObjectType.Blob:
Task.Run(() =>
{
var isBinary = new Commands.IsBinary(_repo, _commit.SHA, file.Path).Result();
if (isBinary)
{
var ext = Path.GetExtension(file.Path);
if (IMG_EXTS.Contains(ext))
{
var stream = Commands.QueryFileContent.Run(_repo, _commit.SHA, file.Path);
var bitmap = stream.Length > 0 ? new Bitmap(stream) : null;
Dispatcher.UIThread.Invoke(() =>
{
ViewRevisionFileContent = new Models.RevisionImageFile() { Image = bitmap };
});
}
else
{
var size = new Commands.QueryFileSize(_repo, file.Path, _commit.SHA).Result();
Dispatcher.UIThread.Invoke(() =>
{
ViewRevisionFileContent = new Models.RevisionBinaryFile() { Size = size };
});
}
return;
}
var contentStream = Commands.QueryFileContent.Run(_repo, _commit.SHA, file.Path);
var content = new StreamReader(contentStream).ReadToEnd();
if (content.StartsWith("version https://git-lfs.github.com/spec/", StringComparison.Ordinal))
{
var obj = new Models.RevisionLFSObject() { Object = new Models.LFSObject() };
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
if (lines.Length == 3)
{
foreach (var line in lines)
{
if (line.StartsWith("oid sha256:", StringComparison.Ordinal))
{
obj.Object.Oid = line.Substring(11);
}
else if (line.StartsWith("size ", StringComparison.Ordinal))
{
obj.Object.Size = long.Parse(line.Substring(5));
}
}
Dispatcher.UIThread.Invoke(() =>
{
ViewRevisionFileContent = obj;
});
return;
}
}
Dispatcher.UIThread.Invoke(() =>
{
ViewRevisionFileContent = new Models.RevisionTextFile()
{
FileName = file.Path,
Content = content
};
});
});
break;
case Models.ObjectType.Commit:
ViewRevisionFileContent = new Models.RevisionSubmodule() { SHA = file.SHA };
break;
default:
ViewRevisionFileContent = null;
break;
}
}
private void BuildRevisionFilesSource(List<Models.FileTreeNode> tree)
{
var source = new HierarchicalTreeDataGridSource<Models.FileTreeNode>(tree)
{
Columns =
{
new HierarchicalExpanderColumn<Models.FileTreeNode>(
new TemplateColumn<Models.FileTreeNode>("Icon", "FileTreeNodeExpanderTemplate", null, GridLength.Auto),
x => x.Children,
x => x.Children.Count > 0,
x => x.IsExpanded),
new TextColumn<Models.FileTreeNode, string>(
null,
x => string.Empty,
GridLength.Star)
}
};
var selection = new Models.TreeDataGridSelectionModel<Models.FileTreeNode>(source, x => x.Children);
selection.SingleSelect = true;
selection.SelectionChanged += (s, _) =>
{
if (s is Models.TreeDataGridSelectionModel<Models.FileTreeNode> selection)
RefreshViewRevisionFile(selection.SelectedItem?.Backend as Models.Object);
};
source.Selection = selection;
RevisionFiles = source;
}
private static readonly HashSet<string> IMG_EXTS = new HashSet<string>()
{
".ico", ".bmp", ".jpg", ".png", ".jpeg"
@ -549,14 +453,12 @@ namespace SourceGit.ViewModels
private string _repo = string.Empty;
private int _activePageIndex = 0;
private Models.Commit _commit = null;
private string _fullMessage = string.Empty;
private List<Models.Change> _changes = null;
private List<Models.Change> _visibleChanges = null;
private List<Models.Change> _selectedChanges = null;
private string _searchChangeFilter = string.Empty;
private DiffContext _diffContext = null;
private List<Models.Object> _revisionFilesBackup = null;
private HierarchicalTreeDataGridSource<Models.FileTreeNode> _revisionFiles = null;
private string _searchFileFilter = string.Empty;
private object _viewRevisionFileContent = null;
private Commands.Command.CancelToken _cancelToken = null;
}

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
@ -58,12 +57,6 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _content, value);
}
public Vector SyncScrollOffset
{
get => _syncScrollOffset;
set => SetProperty(ref _syncScrollOffset, value);
}
public int Unified
{
get => _unified;
@ -138,12 +131,12 @@ namespace SourceGit.ViewModels
if (line.Type == Models.TextDiffLineType.Added)
{
var sha = line.Content.Substring("Subproject commit ".Length);
submoduleDiff.New = new Commands.QuerySingleCommit(submoduleRoot, sha).Result();
submoduleDiff.New = QuerySubmoduleRevision(submoduleRoot, sha);
}
else if (line.Type == Models.TextDiffLineType.Deleted)
{
var sha = line.Content.Substring("Subproject commit ".Length);
submoduleDiff.Old = new Commands.QuerySingleCommit(submoduleRoot, sha).Result();
submoduleDiff.Old = QuerySubmoduleRevision(submoduleRoot, sha);
}
}
rs = submoduleDiff;
@ -164,14 +157,19 @@ namespace SourceGit.ViewModels
var imgDiff = new Models.ImageDiff();
if (_option.Revisions.Count == 2)
{
imgDiff.Old = BitmapFromRevisionFile(_repo, _option.Revisions[0], oldPath);
imgDiff.New = BitmapFromRevisionFile(_repo, _option.Revisions[1], oldPath);
(imgDiff.Old, imgDiff.OldFileSize) = BitmapFromRevisionFile(_repo, _option.Revisions[0], oldPath);
(imgDiff.New, imgDiff.NewFileSize) = BitmapFromRevisionFile(_repo, _option.Revisions[1], oldPath);
}
else
{
var fullPath = Path.Combine(_repo, _option.Path);
imgDiff.Old = BitmapFromRevisionFile(_repo, "HEAD", oldPath);
imgDiff.New = File.Exists(fullPath) ? new Bitmap(fullPath) : null;
(imgDiff.Old, imgDiff.OldFileSize) = BitmapFromRevisionFile(_repo, "HEAD", oldPath);
if (File.Exists(fullPath))
{
imgDiff.New = new Bitmap(fullPath);
imgDiff.NewFileSize = new FileInfo(fullPath).Length;
}
}
rs = imgDiff;
}
@ -203,6 +201,9 @@ namespace SourceGit.ViewModels
Dispatcher.UIThread.Post(() =>
{
if (_content is Models.TextDiff old && rs is Models.TextDiff cur && old.File == cur.File)
cur.SyncScrollOffset = old.SyncScrollOffset;
FileModeChange = latest.FileModeChange;
Content = rs;
IsTextDiff = rs is Models.TextDiff;
@ -211,10 +212,27 @@ namespace SourceGit.ViewModels
});
}
private Bitmap BitmapFromRevisionFile(string repo, string revision, string file)
private (Bitmap, long) BitmapFromRevisionFile(string repo, string revision, string file)
{
var stream = Commands.QueryFileContent.Run(repo, revision, file);
return stream.Length > 0 ? new Bitmap(stream) : null;
var size = stream.Length;
return size > 0 ? (new Bitmap(stream), size) : (null, size);
}
private Models.SubmoduleRevision QuerySubmoduleRevision(string repo, string sha)
{
var commit = new Commands.QuerySingleCommit(repo, sha).Result();
if (commit != null)
{
var body = new Commands.QueryCommitFullMessage(repo, sha).Result();
return new Models.SubmoduleRevision() { Commit = commit, FullMessage = body };
}
return new Models.SubmoduleRevision()
{
Commit = new Models.Commit() { SHA = sha },
FullMessage = string.Empty,
};
}
private static readonly HashSet<string> IMG_EXTS = new HashSet<string>()
@ -229,7 +247,6 @@ namespace SourceGit.ViewModels
private bool _isLoading = true;
private bool _isTextDiff = false;
private object _content = null;
private Vector _syncScrollOffset = Vector.Zero;
private int _unified = 4;
}
}

View file

@ -62,7 +62,7 @@ namespace SourceGit.ViewModels
Task.Run(() =>
{
var commits = new Commands.QueryCommits(_repo, $"-n 10000 -- \"{file}\"").Result();
var commits = new Commands.QueryCommits(_repo, $"-n 10000 -- \"{file}\"", false).Result();
Dispatcher.UIThread.Invoke(() =>
{
IsLoading = false;

View file

@ -1,11 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
@ -24,38 +22,16 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _isLoading, value);
}
public double DataGridRowHeight
{
get => _dataGridRowHeight;
}
public List<Models.Commit> Commits
{
get => _commits;
set
{
var oldAutoSelectedCommitSHA = AutoSelectedCommit?.SHA;
var lastSelected = AutoSelectedCommit;
if (SetProperty(ref _commits, value))
{
Models.Commit newSelectedCommit = null;
if (value.Count > 0 && oldAutoSelectedCommitSHA != null)
{
newSelectedCommit = value.Find(x => x.SHA == oldAutoSelectedCommitSHA);
}
if (newSelectedCommit != AutoSelectedCommit)
{
AutoSelectedCommit = newSelectedCommit;
}
Graph = null;
Task.Run(() =>
{
var graph = Models.CommitGraph.Parse(value, DataGridRowHeight, 8);
Dispatcher.UIThread.Invoke(() =>
{
Graph = graph;
});
});
if (value.Count > 0 && lastSelected != null)
AutoSelectedCommit = value.Find(x => x.SHA == lastSelected.SHA);
}
}
}
@ -652,7 +628,6 @@ namespace SourceGit.ViewModels
}
private Repository _repo = null;
private readonly double _dataGridRowHeight = 28;
private bool _isLoading = true;
private List<Models.Commit> _commits = new List<Models.Commit>();
private Models.CommitGraph _graph = null;

View file

@ -23,6 +23,7 @@ namespace SourceGit.ViewModels
if (SetProperty(ref _activePage, value))
{
PopupHost.Active = value;
UpdateTabSplitterVisible();
}
}
}
@ -157,13 +158,13 @@ namespace SourceGit.ViewModels
ActivePage = Pages[removeIdx == Pages.Count - 1 ? removeIdx - 1 : removeIdx + 1];
CloseRepositoryInTab(page);
Pages.RemoveAt(removeIdx);
OnPropertyChanged(nameof(Pages));
UpdateTabSplitterVisible();
}
else if (removeIdx + 1 == activeIdx)
{
CloseRepositoryInTab(page);
Pages.RemoveAt(removeIdx);
OnPropertyChanged(nameof(Pages));
UpdateTabSplitterVisible();
}
else
{
@ -174,42 +175,25 @@ namespace SourceGit.ViewModels
GC.Collect();
}
public void CloseOtherTabs(object param)
public void CloseOtherTabs()
{
if (Pages.Count == 1)
return;
var page = param as LauncherPage;
if (page == null)
page = _activePage;
ActivePage = page;
var id = ActivePage.Node.Id;
foreach (var one in Pages)
{
if (one.Node.Id != page.Node.Id)
if (one.Node.Id != id)
CloseRepositoryInTab(one);
}
Pages = new AvaloniaList<LauncherPage> { page };
OnPropertyChanged(nameof(Pages));
Pages = new AvaloniaList<LauncherPage> { ActivePage };
GC.Collect();
}
public void CloseRightTabs(object param)
public void CloseRightTabs()
{
LauncherPage page = param as LauncherPage;
if (page == null)
page = _activePage;
var endIdx = Pages.IndexOf(page);
var activeIdx = Pages.IndexOf(_activePage);
if (endIdx < activeIdx)
{
ActivePage = page;
}
var endIdx = Pages.IndexOf(ActivePage);
for (var i = Pages.Count - 1; i > endIdx; i--)
{
CloseRepositoryInTab(Pages[i]);
@ -275,6 +259,13 @@ namespace SourceGit.ViewModels
page.Data = null;
}
private void UpdateTabSplitterVisible()
{
var activePageIdx = ActivePage == null ? -1 : Pages.IndexOf(ActivePage);
for (int i = 0; i < Pages.Count; i++)
Pages[i].IsTabSplitterVisible = (activePageIdx != i && activePageIdx != i + 1);
}
private LauncherPage _activePage = null;
}
}

View file

@ -18,6 +18,12 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _data, value);
}
public bool IsTabSplitterVisible
{
get => _isTabSplitterVisible;
set => SetProperty(ref _isTabSplitterVisible, value);
}
public AvaloniaList<Models.Notification> Notifications
{
get;
@ -50,12 +56,11 @@ namespace SourceGit.ViewModels
public void DismissNotification(object param)
{
if (param is Models.Notification notice)
{
Notifications.Remove(notice);
}
}
private RepositoryNode _node = null;
private object _data = null;
private bool _isTabSplitterVisible = true;
}
}

View file

@ -65,9 +65,7 @@ namespace SourceGit.ViewModels
set
{
if (SetProperty(ref _locale, value))
{
App.SetLocale(value);
}
}
}
@ -77,9 +75,17 @@ namespace SourceGit.ViewModels
set
{
if (SetProperty(ref _theme, value))
{
App.SetTheme(value);
}
App.SetTheme(_theme, _colorOverrides);
}
}
public string ColorOverrides
{
get => _colorOverrides;
set
{
if (SetProperty(ref _colorOverrides, value))
App.SetTheme(_theme, value);
}
}
@ -164,6 +170,12 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _useSyntaxHighlighting, value);
}
public bool EnableDiffViewWordWrap
{
get => _enableDiffViewWordWrap;
set => SetProperty(ref _enableDiffViewWordWrap, value);
}
public Models.ChangeViewMode UnstagedChangeViewMode
{
get => _unstagedChangeViewMode;
@ -515,6 +527,7 @@ namespace SourceGit.ViewModels
private string _locale = "en_US";
private string _theme = "Default";
private string _colorOverrides = string.Empty;
private FontFamily _defaultFont = null;
private FontFamily _monospaceFont = null;
private double _defaultFontSize = 13;
@ -526,6 +539,7 @@ namespace SourceGit.ViewModels
private bool _useTwoColumnsLayoutInHistories = false;
private bool _useSideBySideDiff = false;
private bool _useSyntaxHighlighting = false;
private bool _enableDiffViewWordWrap = false;
private Models.ChangeViewMode _unstagedChangeViewMode = Models.ChangeViewMode.List;
private Models.ChangeViewMode _stagedChangeViewMode = Models.ChangeViewMode.List;

View file

@ -185,6 +185,13 @@ namespace SourceGit.ViewModels
}
}
[JsonIgnore]
public int SearchCommitFilterType
{
get => _searchCommitFilterType;
set => SetProperty(ref _searchCommitFilterType, value);
}
[JsonIgnore]
public string SearchCommitFilter
{
@ -416,19 +423,26 @@ namespace SourceGit.ViewModels
return;
var visible = new List<Models.Commit>();
foreach (var c in _histories.Commits)
if (_searchCommitFilterType == 0)
{
if (c.SHA.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase)
|| c.Subject.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase)
|| c.Message.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase)
|| c.Author.Name.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase)
|| c.Committer.Name.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase)
|| c.Author.Email.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase)
|| c.Committer.Email.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase))
foreach (var c in _histories.Commits)
{
visible.Add(c);
if (c.SHA.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase)
|| c.Subject.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase)
|| c.Author.Name.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase)
|| c.Committer.Name.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase)
|| c.Author.Email.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase)
|| c.Committer.Email.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase))
{
visible.Add(c);
}
}
}
else
{
visible = new Commands.QueryCommits(FullPath, $"-1000 -- \"{_searchCommitFilter}\"", false).Result();
}
SearchedCommits = visible;
}
@ -606,12 +620,15 @@ namespace SourceGit.ViewModels
}
var commits = new Commands.QueryCommits(FullPath, limits).Result();
var graph = Models.CommitGraph.Parse(commits, 8);
Dispatcher.UIThread.Invoke(() =>
{
if (_histories != null)
{
_histories.IsLoading = false;
_histories.Commits = commits;
_histories.Graph = graph;
}
});
}
@ -769,6 +786,12 @@ namespace SourceGit.ViewModels
PopupHost.ShowPopup(new AddSubmodule(this));
}
public void UpdateSubmodules()
{
if (PopupHost.CanCreatePopup())
PopupHost.ShowAndStartPopup(new UpdateSubmodules(this));
}
public ContextMenu CreateContextMenuForGitFlow()
{
var menu = new ContextMenu();
@ -1500,6 +1523,7 @@ namespace SourceGit.ViewModels
private object _selectedView = null;
private bool _isSearching = false;
private int _searchCommitFilterType = 0;
private string _searchCommitFilter = string.Empty;
private List<Models.Commit> _searchedCommits = new List<Models.Commit>();

View file

@ -17,12 +17,6 @@ namespace SourceGit.ViewModels
set;
}
public Models.GPGFormat GPGFormat
{
get;
set;
}
public bool GPGCommitSigningEnabled
{
get;
@ -60,10 +54,6 @@ namespace SourceGit.ViewModels
GPGCommitSigningEnabled = gpgCommitSign == "true";
if (_cached.TryGetValue("tag.gpgSign", out var gpgTagSign))
GPGTagSigningEnabled = gpgTagSign == "true";
if (_cached.TryGetValue("gpg.format", out var gpgFormat))
GPGFormat = Models.GPGFormat.Supported.Find(x => x.Value == gpgFormat);
else
GPGFormat = Models.GPGFormat.OPENPGP;
if (_cached.TryGetValue("user.signingkey", out var signingKey))
GPGUserSigningKey = signingKey;
if (_cached.TryGetValue("http.proxy", out var proxy))
@ -78,20 +68,19 @@ namespace SourceGit.ViewModels
SetIfChanged("user.email", UserEmail);
SetIfChanged("commit.gpgsign", GPGCommitSigningEnabled ? "true" : "false");
SetIfChanged("tag.gpgSign", GPGTagSigningEnabled ? "true" : "false");
SetIfChanged("gpg.format", GPGFormat?.Value, Models.GPGFormat.OPENPGP.Value);
SetIfChanged("user.signingkey", GPGUserSigningKey);
SetIfChanged("http.proxy", HttpProxy);
return null;
}
private void SetIfChanged(string key, string value, string defaultValue = null)
private void SetIfChanged(string key, string value)
{
bool changed = false;
if (_cached.TryGetValue(key, out var old))
{
changed = old != value;
}
else if (!string.IsNullOrEmpty(value) && value != defaultValue)
else if (!string.IsNullOrEmpty(value))
{
changed = true;
}

View file

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
namespace SourceGit.ViewModels
@ -21,14 +22,16 @@ namespace SourceGit.ViewModels
public Reword(Repository repo, Models.Commit head)
{
_repo = repo;
_oldMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, head.SHA).Result();
_message = _oldMessage;
Head = head;
Message = head.FullMessage;
View = new Views.Reword() { DataContext = this };
}
public override Task<bool> Sure()
{
if (_message == Head.FullMessage)
if (string.Compare(_message, _oldMessage, StringComparison.Ordinal) == 0)
return null;
_repo.SetWatcherEnabled(false);
@ -44,5 +47,6 @@ namespace SourceGit.ViewModels
private readonly Repository _repo = null;
private string _message = string.Empty;
private string _oldMessage = string.Empty;
}
}

View file

@ -27,7 +27,8 @@ namespace SourceGit.ViewModels
public Squash(Repository repo, Models.Commit head, Models.Commit parent)
{
_repo = repo;
_message = parent.FullMessage;
_message = new Commands.QueryCommitFullMessage(_repo.FullPath, parent.SHA).Result();
Head = head;
Parent = parent;
View = new Views.Squash() { DataContext = this };

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Threading;

View file

@ -0,0 +1,28 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels
{
public class UpdateSubmodules : Popup
{
public UpdateSubmodules(Repository repo)
{
_repo = repo;
View = new Views.UpdateSubmodules() { DataContext = this };
}
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Updating submodules ...";
return Task.Run(() =>
{
new Commands.Submodule(_repo.FullPath).Update(SetProgressDescription);
CallUIThread(() => _repo.SetWatcherEnabled(true));
return true;
});
}
private readonly Repository _repo = null;
}
}

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
@ -85,17 +84,16 @@ namespace SourceGit.ViewModels
{
if (SetProperty(ref _useAmend, value) && value)
{
var commits = new Commands.QueryCommits(_repo.FullPath, "-n 1", false).Result();
if (commits.Count == 0)
var currentBranch = _repo.Branches.Find(x => x.IsCurrent);
if (currentBranch == null)
{
App.RaiseException(_repo.FullPath, "No commits to amend!!!");
_useAmend = false;
OnPropertyChanged();
return;
}
else
{
CommitMessage = commits[0].FullMessage;
}
CommitMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, currentBranch.Head).Result();
}
OnPropertyChanged(nameof(IsCommitWithPushVisible));
@ -273,10 +271,6 @@ namespace SourceGit.ViewModels
Staged = staged;
_isLoadingData = false;
var scrollOffset = Vector.Zero;
if (_detailContext is DiffContext old)
scrollOffset = old.SyncScrollOffset;
if (selectedUnstaged.Count > 0)
SelectedUnstaged = selectedUnstaged;
else if (selectedStaged.Count > 0)
@ -284,9 +278,6 @@ namespace SourceGit.ViewModels
else
SetDetail(null);
if (_detailContext is DiffContext cur)
cur.SyncScrollOffset = scrollOffset;
// Try to load merge message from MERGE_MSG
if (string.IsNullOrEmpty(_commitMessage))
{

View file

@ -21,7 +21,8 @@ namespace SourceGit.Views
Close();
}
private void OnRemoveButtonClicked(object sender, RoutedEventArgs e) {
private void OnRemoveButtonClicked(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.AssumeUnchangedManager vm && sender is Button button)
vm.Remove(button.DataContext as string);

View file

@ -9,10 +9,10 @@
x:Class="SourceGit.Views.ChangeCollectionView"
x:Name="ThisControl">
<UserControl.Resources>
<DataTemplate x:Key="TreeModeTemplate" DataType="m:FileTreeNode">
<DataTemplate x:Key="TreeModeTemplate" DataType="v:ChangeTreeNode">
<Grid HorizontalAlignment="Stretch" Height="24" ColumnDefinitions="Auto,*">
<Path Grid.Column="0" Classes="folder_icon" Width="14" Height="14" Margin="0,2,0,0" IsVisible="{Binding IsFolder}" Fill="Goldenrod" VerticalAlignment="Center"/>
<v:ChangeStatusIcon Grid.Column="0" Width="14" Height="14" IsWorkingCopyChange="{Binding #ThisControl.IsWorkingCopyChange}" Change="{Binding Backend}" IsVisible="{Binding !IsFolder}"/>
<v:ChangeStatusIcon Grid.Column="0" Width="14" Height="14" IsWorkingCopyChange="{Binding #ThisControl.IsWorkingCopyChange}" Change="{Binding Change}" IsVisible="{Binding !IsFolder}"/>
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding FullPath, Converter={x:Static c:PathConverters.PureFileName}}" Margin="6,0,0,0"/>
</Grid>
</DataTemplate>

View file

@ -9,6 +9,101 @@ using Avalonia.Interactivity;
namespace SourceGit.Views
{
public class ChangeTreeNode
{
public string FullPath { get; set; } = string.Empty;
public bool IsFolder { get; set; } = false;
public bool IsExpanded { get; set; } = false;
public Models.Change Change { get; set; } = null;
public List<ChangeTreeNode> Children { get; set; } = new List<ChangeTreeNode>();
public static List<ChangeTreeNode> Build(IList<Models.Change> changes, bool expanded)
{
var nodes = new List<ChangeTreeNode>();
var folders = new Dictionary<string, ChangeTreeNode>();
foreach (var c in changes)
{
var sepIdx = c.Path.IndexOf('/', StringComparison.Ordinal);
if (sepIdx == -1)
{
nodes.Add(new ChangeTreeNode()
{
FullPath = c.Path,
Change = c,
IsFolder = false,
IsExpanded = false
});
}
else
{
ChangeTreeNode lastFolder = null;
var start = 0;
while (sepIdx != -1)
{
var folder = c.Path.Substring(0, sepIdx);
if (folders.TryGetValue(folder, out var value))
{
lastFolder = value;
}
else if (lastFolder == null)
{
lastFolder = new ChangeTreeNode()
{
FullPath = folder,
IsFolder = true,
IsExpanded = expanded
};
folders.Add(folder, lastFolder);
InsertFolder(nodes, lastFolder);
}
else
{
var cur = new ChangeTreeNode()
{
FullPath = folder,
IsFolder = true,
IsExpanded = expanded
};
folders.Add(folder, cur);
InsertFolder(lastFolder.Children, cur);
lastFolder = cur;
}
start = sepIdx + 1;
sepIdx = c.Path.IndexOf('/', start);
}
lastFolder.Children.Add(new ChangeTreeNode()
{
FullPath = c.Path,
Change = c,
IsFolder = false,
IsExpanded = false
});
}
}
folders.Clear();
return nodes;
}
private static void InsertFolder(List<ChangeTreeNode> collection, ChangeTreeNode subFolder)
{
for (int i = 0; i < collection.Count; i++)
{
if (!collection[i].IsFolder)
{
collection.Insert(i, subFolder);
return;
}
}
collection.Add(subFolder);
}
}
public partial class ChangeCollectionView : UserControl
{
public static readonly StyledProperty<bool> IsWorkingCopyChangeProperty =
@ -85,40 +180,38 @@ namespace SourceGit.Views
Content = null;
var changes = Changes;
if (changes == null)
if (changes == null || changes.Count == 0)
return;
var viewMode = ViewMode;
if (viewMode == Models.ChangeViewMode.Tree)
{
var filetree = Models.FileTreeNode.Build(changes, true);
var filetree = ChangeTreeNode.Build(changes, true);
var template = this.FindResource("TreeModeTemplate") as IDataTemplate;
var source = new HierarchicalTreeDataGridSource<Models.FileTreeNode>(filetree)
var source = new HierarchicalTreeDataGridSource<ChangeTreeNode>(filetree)
{
Columns =
{
new HierarchicalExpanderColumn<Models.FileTreeNode>(
new TemplateColumn<Models.FileTreeNode>(null, template, null, GridLength.Auto),
new HierarchicalExpanderColumn<ChangeTreeNode>(
new TemplateColumn<ChangeTreeNode>(null, template, null, GridLength.Auto),
x => x.Children,
x => x.Children.Count > 0,
x => x.IsExpanded)
}
};
var selection = new Models.TreeDataGridSelectionModel<Models.FileTreeNode>(source, x => x.Children);
var selection = new Models.TreeDataGridSelectionModel<ChangeTreeNode>(source, x => x.Children);
selection.SingleSelect = SingleSelect;
selection.RowDoubleTapped += (_, e) => RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent));
selection.SelectionChanged += (s, _) =>
{
if (!_isSelecting && s is Models.TreeDataGridSelectionModel<Models.FileTreeNode> model)
if (!_isSelecting && s is Models.TreeDataGridSelectionModel<ChangeTreeNode> model)
{
var selected = new List<Models.Change>();
foreach (var c in model.SelectedItems)
CollectChangesInNode(selected, c);
_isSelecting = true;
SetCurrentValue(SelectedChangesProperty, selected);
_isSelecting = false;
TrySetSelected(selected);
}
};
@ -144,9 +237,7 @@ namespace SourceGit.Views
foreach (var c in model.SelectedItems)
selected.Add(c);
_isSelecting = true;
SetCurrentValue(SelectedChangesProperty, selected);
_isSelecting = false;
TrySetSelected(selected);
}
};
@ -172,9 +263,7 @@ namespace SourceGit.Views
foreach (var c in model.SelectedItems)
selected.Add(c);
_isSelecting = true;
SetCurrentValue(SelectedChangesProperty, selected);
_isSelecting = false;
TrySetSelected(selected);
}
};
@ -201,7 +290,7 @@ namespace SourceGit.Views
else
changeSelection.Select(selected);
}
else if (tree.Source.Selection is Models.TreeDataGridSelectionModel<Models.FileTreeNode> treeSelection)
else if (tree.Source.Selection is Models.TreeDataGridSelectionModel<ChangeTreeNode> treeSelection)
{
if (selected == null || selected.Count == 0)
{
@ -214,9 +303,9 @@ namespace SourceGit.Views
foreach (var c in selected)
set.Add(c);
var nodes = new List<Models.FileTreeNode>();
var nodes = new List<ChangeTreeNode>();
foreach (var node in tree.Source.Items)
CollectSelectedNodeByChange(nodes, node as Models.FileTreeNode, set);
CollectSelectedNodeByChange(nodes, node as ChangeTreeNode, set);
if (nodes.Count == 0)
treeSelection.Clear();
@ -238,22 +327,20 @@ namespace SourceGit.Views
};
}
private void CollectChangesInNode(List<Models.Change> outs, Models.FileTreeNode node)
private void CollectChangesInNode(List<Models.Change> outs, ChangeTreeNode node)
{
if (node.IsFolder)
{
foreach (var child in node.Children)
CollectChangesInNode(outs, child);
}
else
else if (!outs.Contains(node.Change))
{
var change = node.Backend as Models.Change;
if (change != null && !outs.Contains(change))
outs.Add(change);
outs.Add(node.Change);
}
}
private void CollectSelectedNodeByChange(List<Models.FileTreeNode> outs, Models.FileTreeNode node, HashSet<object> selected)
private void CollectSelectedNodeByChange(List<ChangeTreeNode> outs, ChangeTreeNode node, HashSet<object> selected)
{
if (node == null)
return;
@ -263,12 +350,40 @@ namespace SourceGit.Views
foreach (var child in node.Children)
CollectSelectedNodeByChange(outs, child, selected);
}
else if (node.Backend != null && selected.Contains(node.Backend))
else if (node.Change != null && selected.Contains(node.Change))
{
outs.Add(node);
}
}
private void TrySetSelected(List<Models.Change> changes)
{
var old = SelectedChanges;
if (old == null && changes.Count == 0)
return;
if (old != null && old.Count == changes.Count)
{
bool allEquals = true;
foreach (var c in old)
{
if (!changes.Contains(c))
{
allEquals = false;
break;
}
}
if (allEquals)
return;
}
_isSelecting = true;
SetCurrentValue(SelectedChangesProperty, changes);
_isSelecting = false;
}
private bool _isSelecting = false;
}
}

View file

@ -7,14 +7,15 @@
xmlns:v="using:SourceGit.Views"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.CommitBaseInfo">
x:Class="SourceGit.Views.CommitBaseInfo"
x:Name="ThisControl">
<UserControl.DataTemplates>
<DataTemplate DataType="m:Commit">
<StackPanel Orientation="Vertical">
<!-- Author & Committer -->
<UniformGrid Rows="1" Margin="0,8">
<!-- Author -->
<Grid Grid.Column="0" ColumnDefinitions="96,*">
<Grid ColumnDefinitions="96,*">
<v:Avatar Grid.Column="0" Width="64" Height="64" HorizontalAlignment="Right" User="{Binding Author}"/>
<StackPanel Grid.Column="1" Margin="16,0,8,0" Orientation="Vertical">
<TextBlock Classes="group_header_label" Margin="0" Text="{DynamicResource Text.CommitDetail.Info.Author}"/>
@ -30,7 +31,7 @@
</Grid>
<!-- Committer -->
<Grid Grid.Column="1" ColumnDefinitions="96,*" IsVisible="{Binding IsCommitterVisible}">
<Grid ColumnDefinitions="96,*" IsVisible="{Binding IsCommitterVisible}">
<v:Avatar Grid.Column="0" Width="64" Height="64" HorizontalAlignment="Right" User="{Binding Committer}"/>
<StackPanel Grid.Column="1" Margin="16,0,8,0" Orientation="Vertical">
<TextBlock Classes="group_header_label" Margin="0" Text="{DynamicResource Text.CommitDetail.Info.Committer}"/>
@ -104,7 +105,7 @@
<!-- Messages -->
<TextBlock Grid.Row="3" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.Message}" VerticalAlignment="Top" Margin="0,4,0,0" />
<ScrollViewer Grid.Row="3" Grid.Column="1" Margin="12,5,8,0" MaxHeight="64" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<SelectableTextBlock Text="{Binding FullMessage}" FontFamily="{Binding Source={x:Static vm:Preference.Instance}, Path=MonospaceFont}" TextWrapping="Wrap"/>
<SelectableTextBlock Text="{Binding #ThisControl.Message}" FontFamily="{Binding Source={x:Static vm:Preference.Instance}, Path=MonospaceFont}" TextWrapping="Wrap"/>
</ScrollViewer>
</Grid>
</StackPanel>

View file

@ -15,6 +15,15 @@ namespace SourceGit.Views
set => SetValue(CanNavigateProperty, value);
}
public static readonly StyledProperty<string> MessageProperty =
AvaloniaProperty.Register<CommitBaseInfo, string>(nameof(Message), string.Empty);
public string Message
{
get => GetValue(MessageProperty);
set => SetValue(MessageProperty, value);
}
public CommitBaseInfo()
{
InitializeComponent();

View file

@ -19,7 +19,7 @@
<Grid RowDefinitions="Auto,1,*">
<!-- Base Information -->
<v:CommitBaseInfo Grid.Row="0" Content="{Binding Commit}"/>
<v:CommitBaseInfo Grid.Row="0" Content="{Binding Commit}" Message="{Binding FullMessage}"/>
<!-- Line -->
<Rectangle Grid.Row="1" Height=".65" Margin="8" Fill="{DynamicResource Brush.Border2}" VerticalAlignment="Center"/>
@ -36,16 +36,25 @@
HeadersVisibility="None"
Focusable="False"
RowHeight="26"
Margin="80,0,8,16"
Margin="64,0,8,16"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"
ContextRequested="OnChangeListContextRequested"
DoubleTapped="OnChangeListDoubleTapped">
<DataGrid.Styles>
<Style Selector="DataGridRow">
<Setter Property="CornerRadius" Value="4"/>
</Style>
<Style Selector="DataGridRow /template/ Border#RowBorder">
<Setter Property="ClipToBounds" Value="True"/>
</Style>
</DataGrid.Styles>
<DataGrid.Columns>
<DataGridTemplateColumn Header="ICON">
<DataGridTemplateColumn Width="36" Header="ICON">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<v:ChangeStatusIcon Width="14" Height="14" IsWorkingCopyChange="False" Change="{Binding}"/>
<v:ChangeStatusIcon Width="14" Height="14" HorizontalAlignment="Left" Margin="16,0,0,0" IsWorkingCopyChange="False" Change="{Binding}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

View file

@ -16,7 +16,7 @@ namespace SourceGit.Views
{
var datagrid = sender as DataGrid;
detail.ActivePageIndex = 1;
detail.SelectedChanges = new () { datagrid.SelectedItem as Models.Change };
detail.SelectedChanges = new() { datagrid.SelectedItem as Models.Change };
}
e.Handled = true;

View file

@ -8,7 +8,8 @@ namespace SourceGit.Views
{
public static void OpenContextMenu(this Control control, ContextMenu menu)
{
if (menu == null) return;
if (menu == null)
return;
menu.PlacementTarget = control;
menu.Closing += OnContextMenuClosing; // Clear context menu because it is dynamic.

View file

@ -30,7 +30,7 @@
</Border>
<!-- Title -->
<TextBlock Grid.Column="2" Classes="monospace" Margin="4,0,0,0" Text="{Binding Title}" FontSize="11"/>
<TextBlock Grid.Column="2" Classes="monospace" Margin="4,0,0,0" Text="{Binding Title}" FontSize="11" TextTrimming="CharacterEllipsis"/>
<!-- Toolbar Buttons -->
<StackPanel Grid.Column="3" Margin="8,0,0,0" Orientation="Horizontal" VerticalAlignment="Center">
@ -52,6 +52,16 @@
<Path Width="13" Height="13" Data="{StaticResource Icons.SyntaxHighlight}" Margin="0,3,0,0"/>
</ToggleButton>
<ToggleButton Classes="line_path"
Width="32" Height="18"
Background="Transparent"
Padding="9,6"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap, Mode=TwoWay}"
IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.ToggleWordWrap}">
<Path Width="12" Height="12" Data="{StaticResource Icons.WordWrap}" Margin="0,2,0,0"/>
</ToggleButton>
<ToggleButton Classes="line_path"
Width="32" Height="18"
Background="Transparent"
@ -141,9 +151,9 @@
<Border IsVisible="{Binding Old, Converter={x:Static ObjectConverters.IsNotNull}}">
<ContentControl Content="{Binding Old}">
<ContentControl.DataTemplates>
<DataTemplate DataType="m:Commit">
<DataTemplate DataType="m:SubmoduleRevision">
<Border Margin="0,0,0,8" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}" Background="{DynamicResource Brush.Window}">
<v:CommitBaseInfo MaxHeight="256" Margin="0,0,0,4" CanNavigate="False" Content="{Binding}"/>
<v:CommitBaseInfo MaxHeight="256" Margin="0,0,0,4" CanNavigate="False" Content="{Binding Commit}" Message="{Binding FullMessage}"/>
</Border>
</DataTemplate>
</ContentControl.DataTemplates>
@ -157,7 +167,7 @@
<Path Width="16" Height="16" Data="{StaticResource Icons.DoubleDown}" HorizontalAlignment="Center" IsVisible="{Binding Old, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<Border Margin="0,8,0,0" BorderThickness="1" BorderBrush="Green" Background="{DynamicResource Brush.Window}">
<v:CommitBaseInfo MaxHeight="256" Margin="0,0,0,4" CanNavigate="False" Content="{Binding New}"/>
<v:CommitBaseInfo MaxHeight="256" Margin="0,0,0,4" CanNavigate="False" Content="{Binding New.Commit}" Message="{Binding New.FullMessage}"/>
</Border>
</StackPanel>
</ScrollViewer>
@ -166,58 +176,70 @@
<!-- Image Diff -->
<DataTemplate DataType="m:ImageDiff">
<Grid RowDefinitions="Auto,*,Auto" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8,8,8,0">
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<Grid RowDefinitions="Auto,*" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8,16,8,0">
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto" HorizontalAlignment="Center">
<Border Grid.Column="0" Height="16" Background="{DynamicResource Brush.Badge}" CornerRadius="8" VerticalAlignment="Center">
<TextBlock Classes="monospace" Text="{DynamicResource Text.Diff.Binary.Old}" Margin="8,0" FontSize="10"/>
</Border>
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding OldSize}" Foreground="{DynamicResource Brush.FG2}" Margin="8,0,0,0"/>
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding OldImageSize}" Margin="8,0,0,0"/>
<TextBlock Grid.Column="2" Classes="monospace" Text="{Binding OldFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
<TextBlock Grid.Column="3" Classes="monospace" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
<Border Grid.Column="3" Height="16" Background="Green" CornerRadius="8" VerticalAlignment="Center" Margin="32,0,0,0">
<Border Grid.Column="4" Height="16" Background="Green" CornerRadius="8" VerticalAlignment="Center" Margin="32,0,0,0">
<TextBlock Classes="monospace" Text="{DynamicResource Text.Diff.Binary.New}" Margin="8,0" FontSize="10"/>
</Border>
<TextBlock Grid.Column="4" Classes="monospace" Text="{Binding NewSize}" Foreground="{DynamicResource Brush.FG2}" Margin="8,0,0,0"/>
<TextBlock Grid.Column="5" Classes="monospace" Text="{Binding NewImageSize}" Margin="8,0,0,0"/>
<TextBlock Grid.Column="6" Classes="monospace" Text="{Binding NewFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
<TextBlock Grid.Column="7" Classes="monospace" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
</Grid>
<Border Grid.Row="1" Background="{DynamicResource Brush.Window}" Effect="drop-shadow(0 0 8 #A0000000)" Margin="0,8,0,0" HorizontalAlignment="Center">
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}" Margin="8">
<v:ImageDiffView Alpha="{Binding #ImageDiffSlider.Value}"
OldImage="{Binding Old}"
NewImage="{Binding New}"
RenderOptions.BitmapInterpolationMode="HighQuality"/>
<Grid Grid.Row="1" RowDefinitions="*,Auto,Auto" Margin="0,16,0,0" HorizontalAlignment="Center">
<Border Grid.Row="0" Background="{DynamicResource Brush.Window}" Effect="drop-shadow(0 0 8 #A0000000)">
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}" Margin="8">
<v:ImageDiffView Alpha="{Binding #ImageDiffSlider.Value}"
OldImage="{Binding Old}"
NewImage="{Binding New}"
RenderOptions.BitmapInterpolationMode="HighQuality"/>
</Border>
</Border>
</Border>
<Slider Grid.Row="2"
x:Name="ImageDiffSlider"
Minimum="0" Maximum="1"
VerticalAlignment="Top"
TickPlacement="None"
Margin="0,4,0,0"
MinHeight="0"
Foreground="{DynamicResource Brush.Border1}"
Value="0.5">
<Slider.Resources>
<Thickness x:Key="SliderTopHeaderMargin">0,0,0,4</Thickness>
<GridLength x:Key="SliderPreContentMargin">0</GridLength>
<GridLength x:Key="SliderPostContentMargin">0</GridLength>
<CornerRadius x:Key="SliderThumbCornerRadius">8</CornerRadius>
<x:Double x:Key="SliderHorizontalThumbWidth">16</x:Double>
<x:Double x:Key="SliderHorizontalThumbHeight">16</x:Double>
</Slider.Resources>
</Slider>
<Grid Grid.Row="1" ColumnDefinitions="*,*" Margin="0,8,0,0">
<TextBlock Grid.Column="0" Classes="monospace" Text="{DynamicResource Text.Diff.Binary.Old}" Foreground="{DynamicResource Brush.FG2}" FontSize="10"/>
<TextBlock Grid.Column="1" Classes="monospace" Text="{DynamicResource Text.Diff.Binary.New}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Right" FontSize="10"/>
</Grid>
<Slider Grid.Row="2"
x:Name="ImageDiffSlider"
Minimum="0" Maximum="1"
VerticalAlignment="Top"
TickPlacement="None"
Margin="8,4,8,0"
MinHeight="0"
Foreground="{DynamicResource Brush.Border1}"
Value="0.5">
<Slider.Resources>
<Thickness x:Key="SliderTopHeaderMargin">0,0,0,4</Thickness>
<GridLength x:Key="SliderPreContentMargin">0</GridLength>
<GridLength x:Key="SliderPostContentMargin">0</GridLength>
<CornerRadius x:Key="SliderThumbCornerRadius">8</CornerRadius>
<x:Double x:Key="SliderHorizontalThumbWidth">16</x:Double>
<x:Double x:Key="SliderHorizontalThumbHeight">16</x:Double>
</Slider.Resources>
</Slider>
</Grid>
</Grid>
</DataTemplate>
<!-- Text Diff -->
<DataTemplate DataType="m:TextDiff">
<v:TextDiffView TextDiff="{Binding}"
SyncScrollOffset="{Binding SyncScrollOffset, Mode=TwoWay}"
UseSideBySideDiff="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSideBySideDiff, Mode=OneWay}"/>
</DataTemplate>
<!-- No or only EOL changes -->
<!-- Empty or only EOL changes -->
<DataTemplate DataType="m:NoOrEOLChange">
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<Path Width="64" Height="64" Data="{StaticResource Icons.Check}" Fill="{DynamicResource Brush.FG2}"/>

View file

@ -23,7 +23,7 @@
IsReadOnly="True"
HeadersVisibility="None"
Focusable="False"
RowHeight="{Binding DataGridRowHeight}"
RowHeight="28"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"
LayoutUpdated="OnCommitDataGridLayoutUpdated"

View file

@ -69,17 +69,6 @@ namespace SourceGit.Views
public class CommitGraph : Control
{
public static readonly Pen[] Pens = [
new Pen(Brushes.Orange, 2),
new Pen(Brushes.ForestGreen, 2),
new Pen(Brushes.Gold, 2),
new Pen(Brushes.Magenta, 2),
new Pen(Brushes.Red, 2),
new Pen(Brushes.Gray, 2),
new Pen(Brushes.Turquoise, 2),
new Pen(Brushes.Olive, 2),
];
public static readonly StyledProperty<Models.CommitGraph> GraphProperty =
AvaloniaProperty.Register<CommitGraph, Models.CommitGraph>(nameof(Graph));
@ -151,7 +140,7 @@ namespace SourceGit.Views
if (dot.Center.Y > bottom)
break;
context.DrawEllipse(dotFill, Pens[dot.Color], dot.Center, 3, 3);
context.DrawEllipse(dotFill, Models.CommitGraph.Pens[dot.Color], dot.Center, 3, 3);
}
}
@ -168,7 +157,7 @@ namespace SourceGit.Views
continue;
var geo = new StreamGeometry();
var pen = Pens[line.Color];
var pen = Models.CommitGraph.Pens[line.Color];
using (var ctx = geo.Open())
{
var started = false;
@ -238,7 +227,7 @@ namespace SourceGit.Views
ctx.QuadraticBezierTo(link.Control, link.End);
}
context.DrawGeometry(null, Pens[link.Color], geo);
context.DrawGeometry(null, Models.CommitGraph.Pens[link.Color], geo);
}
}
}

View file

@ -125,11 +125,9 @@
CommandParameter="{Binding}"
InputGesture="{OnPlatform Ctrl+W, macOS=⌘+W}"/>
<MenuItem Header="{DynamicResource Text.PageTabBar.Tab.CloseOther}"
Command="{Binding #me.((vm:Launcher)DataContext).CloseOtherTabs}"
CommandParameter="{Binding}"/>
Command="{Binding #me.((vm:Launcher)DataContext).CloseOtherTabs}"/>
<MenuItem Header="{DynamicResource Text.PageTabBar.Tab.CloseRight}"
Command="{Binding #me.((vm:Launcher)DataContext).CloseRightTabs}"
CommandParameter="{Binding}"/>
Command="{Binding #me.((vm:Launcher)DataContext).CloseRightTabs}"/>
<MenuItem Header="-" IsVisible="{Binding Node.IsRepository}"/>
<MenuItem IsVisible="{Binding Node.IsRepository}">
<MenuItem.Header>
@ -210,15 +208,11 @@
</ToolTip.Tip>
<Path Width="8" Height="8" Data="{StaticResource Icons.Window.Close}"/>
</Button>
<Rectangle Grid.Column="2" Width=".5" Height="20" HorizontalAlignment="Right" VerticalAlignment="Center" Fill="{DynamicResource Brush.FG2}">
<Rectangle.IsVisible>
<MultiBinding Converter="{x:Static c:LauncherPageConverters.ToTabSeperatorVisible}">
<Binding/>
<Binding Path="$parent[ListBox].SelectedItem"/>
<Binding Path="#me.((vm:Launcher)DataContext).Pages"/>
</MultiBinding>
</Rectangle.IsVisible>
</Rectangle>
<Rectangle Grid.Column="2"
Width=".5" Height="20"
HorizontalAlignment="Right" VerticalAlignment="Center"
Fill="{DynamicResource Brush.FG2}"
IsVisible="{Binding IsTabSplitterVisible}"/>
</Grid>
</Border>
</DataTemplate>

View file

@ -144,7 +144,7 @@
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preference.Appearance}"/>
</TabItem.Header>
<Grid Margin="8" RowDefinitions="32,32,32,32" ColumnDefinitions="Auto,*">
<Grid Margin="8" RowDefinitions="32,32,32,32,32" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preference.Appearance.Theme}"
HorizontalAlignment="Right"
@ -224,6 +224,21 @@
</Style>
</NumericUpDown.Styles>
</NumericUpDown>
<TextBlock Grid.Row="4" Grid.Column="0"
Text="{DynamicResource Text.Preference.Appearance.ColorOverrides}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<TextBox Grid.Row="4" Grid.Column="1"
Height="28"
CornerRadius="3"
Text="{Binding ColorOverrides, Mode=TwoWay}">
<TextBox.InnerRightContent>
<Button Classes="icon_button" Width="30" Height="30" Click="SelectColorSchemaFile">
<Path Data="{StaticResource Icons.Folder.Open}" Fill="{DynamicResource Brush.FG1}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
</Grid>
</TabItem>
@ -409,7 +424,7 @@
<ac:EnumToBoolConverter x:Key="EnumToBoolConverter"/>
</TabItem.Resources>
<Grid Margin="8" RowDefinitions="32,32,32,32,32" ColumnDefinitions="Auto,*">
<Grid Margin="8" RowDefinitions="32,Auto,32,32,32" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preference.GPG.Format}"
HorizontalAlignment="Right"
@ -419,7 +434,7 @@
Padding="8,0"
HorizontalAlignment="Stretch"
ItemsSource="{Binding Source={x:Static m:GPGFormat.Supported}}"
SelectedItem="{Binding #me.GPGFormat, Mode=TwoWay, FallbackValue={x:Static m:GPGFormat.OPENPGP}}">
SelectedItem="{Binding #me.GPGFormat, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="{x:Type m:GPGFormat}">
<Grid ColumnDefinitions="Auto,*">
@ -433,12 +448,14 @@
<TextBlock Grid.Row="1" Grid.Column="0"
Text="{DynamicResource Text.Preference.GPG.Path}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
Margin="0,0,16,0"
IsVisible="{Binding #me.GPGFormat.NeedFindProgram}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="28"
CornerRadius="3"
Text="{Binding #me.GPGExecutableFile, Mode=TwoWay}"
IsEnabled="{Binding #me.GPGFormat, Mode=TwoWay, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static m:GPGFormat.OPENPGP}}">
Watermark="{DynamicResource Text.Preference.GPG.Path.Placeholder}"
IsVisible="{Binding #me.GPGFormat.NeedFindProgram}">
<TextBox.InnerRightContent>
<Button Classes="icon_button" Width="30" Height="30" Click="SelectGPGExecutable">
<Path Data="{StaticResource Icons.Folder.Open}" Fill="{DynamicResource Brush.FG1}"/>

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
@ -67,7 +68,7 @@ namespace SourceGit.Views
}
public static readonly StyledProperty<Models.GPGFormat> GPGFormatProperty =
AvaloniaProperty.Register<Preference, Models.GPGFormat>(nameof(GPGFormat));
AvaloniaProperty.Register<Preference, Models.GPGFormat>(nameof(GPGFormat), Models.GPGFormat.Supported[0]);
public Models.GPGFormat GPGFormat
{
@ -160,10 +161,11 @@ namespace SourceGit.Views
if (config.TryGetValue("tag.gpgSign", out var gpgTagSign))
EnableGPGTagSigning = (gpgTagSign == "true");
if (config.TryGetValue("gpg.format", out var gpgFormat))
GPGFormat = Models.GPGFormat.Supported.Find(x => x.Value == gpgFormat);
else
GPGFormat = Models.GPGFormat.OPENPGP;
if (config.TryGetValue("gpg.program", out var gpgProgram))
GPGFormat = Models.GPGFormat.Supported.Find(x => x.Value == gpgFormat) ?? Models.GPGFormat.Supported[0];
if (GPGFormat.Value == "opengpg" && config.TryGetValue("gpg.program", out var opengpg))
GPGExecutableFile = opengpg;
else if (config.TryGetValue($"gpg.{GPGFormat.Value}.program", out var gpgProgram))
GPGExecutableFile = gpgProgram;
ver = new Commands.Version().Query();
@ -187,7 +189,7 @@ namespace SourceGit.Views
var oldEmail = config.TryGetValue("user.email", out var email) ? email : string.Empty;
var oldGPGSignKey = config.TryGetValue("user.signingkey", out var signingKey) ? signingKey : string.Empty;
var oldCRLF = config.TryGetValue("core.autocrlf", out var crlf) ? crlf : string.Empty;
var oldGPGFormat = config.TryGetValue("gpg.format", out var gpgFormat) ? gpgFormat : Models.GPGFormat.OPENPGP.Value;
var oldGPGFormat = config.TryGetValue("gpg.format", out var gpgFormat) ? gpgFormat : "opengpg";
var oldGPGCommitSignEnable = config.TryGetValue("commit.gpgsign", out var gpgCommitSign) ? gpgCommitSign : "false";
var oldGPGTagSignEnable = config.TryGetValue("tag.gpgSign", out var gpgTagSign) ? gpgTagSign : "false";
var oldGPGExec = config.TryGetValue("gpg.program", out var program) ? program : string.Empty;
@ -204,14 +206,31 @@ namespace SourceGit.Views
cmd.Set("commit.gpgsign", EnableGPGCommitSigning ? "true" : "false");
if (EnableGPGTagSigning != (oldGPGTagSignEnable == "true"))
cmd.Set("tag.gpgSign", EnableGPGTagSigning ? "true" : "false");
if (GPGFormat != null && GPGFormat.Value != oldGPGFormat)
if (GPGFormat.Value != oldGPGFormat)
cmd.Set("gpg.format", GPGFormat.Value);
if (GPGExecutableFile != oldGPGExec)
cmd.Set("gpg.program", GPGExecutableFile);
cmd.Set($"gpg.{GPGFormat.Value}.program", GPGExecutableFile);
Close();
}
private async void SelectColorSchemaFile(object sender, RoutedEventArgs e)
{
var options = new FilePickerOpenOptions()
{
FileTypeFilter = [new FilePickerFileType("Theme Color Schema File") { Patterns = ["*.json"] }],
AllowMultiple = false,
};
var selected = await StorageProvider.OpenFilePickerAsync(options);
if (selected.Count == 1)
{
ViewModels.Preference.Instance.ColorOverrides = selected[0].Path.LocalPath;
}
e.Handled = true;
}
private async void SelectGitExecutable(object sender, RoutedEventArgs e)
{
var pattern = OperatingSystem.IsWindows() ? "git.exe" : "git";
@ -245,15 +264,13 @@ namespace SourceGit.Views
{
var patterns = new List<string>();
if (OperatingSystem.IsWindows())
patterns.Add("gpg.exe");
else if (OperatingSystem.IsLinux())
patterns.AddRange(new string[] { "gpg", "gpg2" });
patterns.Add($"{GPGFormat.Program}.exe");
else
patterns.Add("gpg");
patterns.Add(GPGFormat.Program);
var options = new FilePickerOpenOptions()
{
FileTypeFilter = [new FilePickerFileType("GPG Executable") { Patterns = patterns }],
FileTypeFilter = [new FilePickerFileType("GPG Program") { Patterns = patterns }],
AllowMultiple = false,
};

View file

@ -212,12 +212,21 @@
<Setter Property="CornerRadius" Value="{Binding CornerRadius}"/>
</Style>
<Style Selector="Grid.repository_leftpanel TreeViewItem:selected /template/ Border#PART_LayoutRoot">
<Style Selector="Grid.repository_leftpanel TreeViewItem /template/ Border#PART_LayoutRoot:pointerover Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within TreeViewItem:selected /template/ Border#PART_LayoutRoot">
<Style Selector="Grid.repository_leftpanel TreeViewItem:selected /template/ Border#PART_LayoutRoot Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within TreeViewItem:selected /template/ Border#PART_LayoutRoot Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within TreeViewItem:selected /template/ Border#PART_LayoutRoot:pointerover Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".8"/>
</Style>
</TreeView.Styles>
<TreeView.ItemTemplate>
@ -276,12 +285,21 @@
<Setter Property="CornerRadius" Value="{Binding CornerRadius}"/>
</Style>
<Style Selector="Grid.repository_leftpanel TreeViewItem:selected /template/ Border#PART_LayoutRoot">
<Style Selector="Grid.repository_leftpanel TreeViewItem /template/ Border#PART_LayoutRoot:pointerover Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within TreeViewItem:selected /template/ Border#PART_LayoutRoot">
<Style Selector="Grid.repository_leftpanel TreeViewItem:selected /template/ Border#PART_LayoutRoot Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within TreeViewItem:selected /template/ Border#PART_LayoutRoot Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within TreeViewItem:selected /template/ Border#PART_LayoutRoot:pointerover Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".8"/>
</Style>
</TreeView.Styles>
@ -345,12 +363,21 @@
<Setter Property="ClipToBounds" Value="True" />
</Style>
<Style Selector="Grid.repository_leftpanel DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value=".5"/>
</Style>
<Style Selector="Grid.repository_leftpanel DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within DataGridRow:selected:pointerover /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".8"/>
</Style>
</DataGrid.Styles>
@ -395,12 +422,17 @@
Classes="icon_button"
Width="14"
Margin="8,0"
Click="UpdateSubmodules"
Command="{Binding UpdateSubmodules}"
IsVisible="{Binding Submodules, Converter={x:Static c:ListConverters.IsNotNullOrEmpty}}"
ToolTip.Tip="{DynamicResource Text.Repository.Submodules.Update}">
<Path x:Name="iconSubmoduleUpdate" Width="12" Height="12" Data="{StaticResource Icons.Loading}"/>
</Button>
<Button Grid.Column="3" Classes="icon_button" Width="14" Margin="0,0,8,0" Command="{Binding AddSubmodule}" ToolTip.Tip="{DynamicResource Text.Repository.Submodules.Add}">
<Button Grid.Column="3"
Classes="icon_button"
Width="14"
Margin="0,0,8,0"
Command="{Binding AddSubmodule}"
ToolTip.Tip="{DynamicResource Text.Repository.Submodules.Add}">
<Path Width="12" Height="12" Data="{StaticResource Icons.Submodule.Add}"/>
</Button>
</Grid>
@ -461,17 +493,17 @@
</Grid>
<!-- Left Search Mode -->
<Grid Grid.Column="0" RowDefinitions="32,*" IsVisible="{Binding IsSearching}" PropertyChanged="OnSearchCommitPanelPropertyChanged">
<Grid Grid.Column="0" RowDefinitions="32,32,*" IsVisible="{Binding IsSearching}" PropertyChanged="OnSearchCommitPanelPropertyChanged">
<!-- Search -->
<TextBox Grid.Row="0"
x:Name="txtSearchCommitsBox"
Margin="4,2"
Margin="4,2,4,0"
Height="24"
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border2}"
Background="{DynamicResource Brush.Contents}"
CornerRadius="4"
Watermark="{DynamicResource Text.Repository.SearchTip}"
Watermark="{DynamicResource Text.Repository.Search}"
Text="{Binding SearchCommitFilter, Mode=TwoWay}"
VerticalContentAlignment="Center"
KeyDown="OnSearchKeyDown">
@ -497,7 +529,27 @@
</TextBox.InnerRightContent>
</TextBox>
<DataGrid Grid.Row="1"
<Grid Grid.Row="1" ColumnDefinitions="Auto,*" Margin="4,0">
<TextBlock Grid.Column="0"
Text="{DynamicResource Text.Repository.Search.By}"
Foreground="{DynamicResource Brush.FG2}"
Margin="2,0,0,0"/>
<ComboBox Grid.Column="1"
MinHeight="24" Height="24"
Padding="8,0"
Background="{DynamicResource Brush.Contents}"
BorderBrush="{DynamicResource Brush.Border2}"
HorizontalAlignment="Right"
SelectedIndex="{Binding SearchCommitFilterType, Mode=TwoWay}">
<ComboBox.Items>
<TextBlock Text="{DynamicResource Text.Repository.Search.ByBaseInfo}" FontSize="12"/>
<TextBlock Text="{DynamicResource Text.Repository.Search.ByFile}" FontSize="12"/>
</ComboBox.Items>
</ComboBox>
</Grid>
<DataGrid Grid.Row="2"
ItemsSource="{Binding SearchedCommits}"
SelectionMode="Single"
SelectedItem="{Binding SearchResultSelectedCommit, Mode=OneWay}"
@ -512,6 +564,7 @@
BorderBrush="{DynamicResource Brush.Border2}"
Background="{DynamicResource Brush.Contents}"
Margin="4,0,4,4"
CornerRadius="4"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"
SelectionChanged="OnSearchResultDataGridSelectionChanged">
@ -540,7 +593,7 @@
</DataGrid.Columns>
</DataGrid>
<Path Grid.Row="1"
<Path Grid.Row="2"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="48" Height="48"
Data="{StaticResource Icons.Empty}"

View file

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
@ -306,20 +304,6 @@ namespace SourceGit.Views
e.Handled = true;
}
private async void UpdateSubmodules(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.Repository repo)
{
repo.SetWatcherEnabled(false);
iconSubmoduleUpdate.Classes.Add("rotating");
await Task.Run(() => new Commands.Submodule(repo.FullPath).Update());
iconSubmoduleUpdate.Classes.Remove("rotating");
repo.SetWatcherEnabled(true);
}
e.Handled = true;
}
private void CollectBranchesFromNode(List<Models.Branch> outs, ViewModels.BranchTreeNode node)
{
if (node == null || node.IsRemote)

View file

@ -14,7 +14,7 @@
Classes="bold"
Text="{DynamicResource Text.Configure}"/>
<Grid Margin="0,16,0,0" RowDefinitions="32,32,32,32,32,32,32" ColumnDefinitions="150,*">
<Grid Margin="0,16,0,0" RowDefinitions="32,32,32,32,32,32" ColumnDefinitions="150,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
@ -47,40 +47,20 @@
Text="{Binding HttpProxy, Mode=TwoWay}"/>
<TextBlock Grid.Row="3" Grid.Column="0"
Text="{DynamicResource Text.Preference.GPG.Format}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<ComboBox Grid.Row="3" Grid.Column="1"
MinHeight="28"
Padding="8,0"
HorizontalAlignment="Stretch"
ItemsSource="{Binding Source={x:Static m:GPGFormat.Supported}}"
SelectedItem="{Binding GPGFormat, Mode=TwoWay, FallbackValue={x:Static m:GPGFormat.OPENPGP}}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="{x:Type m:GPGFormat}">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" Text="{Binding Name}"/>
<TextBlock Grid.Column="1" Text="{Binding Desc}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Right"/>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="4" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.Preference.GPG.UserKey}"/>
<TextBox Grid.Row="4" Grid.Column="1"
<TextBox Grid.Row="3" Grid.Column="1"
Height="28"
CornerRadius="3"
Watermark="{DynamicResource Text.Preference.GPG.UserKey.Placeholder}"
Text="{Binding GPGUserSigningKey, Mode=TwoWay}"/>
<CheckBox Grid.Row="5" Grid.Column="1"
<CheckBox Grid.Row="4" Grid.Column="1"
Content="{DynamicResource Text.Preference.GPG.CommitEnabled}"
IsChecked="{Binding GPGCommitSigningEnabled, Mode=TwoWay}"/>
<CheckBox Grid.Row="6" Grid.Column="1"
<CheckBox Grid.Row="5" Grid.Column="1"
Content="{DynamicResource Text.Preference.GPG.TagEnabled}"
IsChecked="{Binding GPGTagSigningEnabled, Mode=TwoWay}"/>
</Grid>

View file

@ -20,8 +20,8 @@
IsHitTestVisible="False"
User="{Binding StartPoint.Author}"/>
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding StartPoint.Author.Name}" Margin="8,0,0,0"/>
<Border Grid.Column="2" Background="DarkGreen" CornerRadius="4" IsVisible="{Binding StartPoint.IsCurrentHead}">
<TextBlock Text="HEAD" Classes="monospace" Margin="4,0"/>
<Border Grid.Column="2" Background="{DynamicResource Brush.Accent}" CornerRadius="4" IsVisible="{Binding StartPoint.IsCurrentHead}">
<TextBlock Text="HEAD" Classes="monospace" Margin="4,0" Foreground="#FFDDDDDD"/>
</Border>
<TextBlock Grid.Column="3" Classes="monospace" Text="{Binding StartPoint.SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" Foreground="DarkOrange" Margin="8,0,0,0" TextDecorations="Underline" PointerPressed="OnPressedSHA"/>
<TextBlock Grid.Column="4" Classes="monospace" Text="{Binding StartPoint.CommitterTimeStr}" Foreground="{DynamicResource Brush.FG2}" Margin="8,0,0,0"/>
@ -44,8 +44,8 @@
IsHitTestVisible="False"
User="{Binding Author}"/>
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding Author.Name}" Margin="8,0,0,0"/>
<Border Grid.Column="2" Background="DarkGreen" CornerRadius="4" IsVisible="{Binding IsCurrentHead}">
<TextBlock Text="HEAD" Classes="monospace" Margin="4,0"/>
<Border Grid.Column="2" Background="{DynamicResource Brush.Accent}" CornerRadius="4" IsVisible="{Binding IsCurrentHead}">
<TextBlock Text="HEAD" Classes="monospace" Margin="4,0" Foreground="#FFDDDDDD"/>
</Border>
<TextBlock Grid.Column="3" Classes="monospace" Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" Foreground="DarkOrange" Margin="8,0,0,0" TextDecorations="Underline" PointerPressed="OnPressedSHA" />
<TextBlock Grid.Column="4" Classes="monospace" Text="{Binding CommitterTimeStr}" Foreground="{DynamicResource Brush.FG2}" Margin="8,0,0,0"/>
@ -56,8 +56,8 @@
</DataTemplate>
<DataTemplate DataType="vm:CompareTargetWorktree">
<Border HorizontalAlignment="Center" VerticalAlignment="Center" Background="DarkGreen" CornerRadius="4">
<TextBlock Text="{DynamicResource Text.Worktree}" Classes="monospace" Margin="4,2"/>
<Border HorizontalAlignment="Center" VerticalAlignment="Center" Background="{DynamicResource Brush.Accent}" CornerRadius="4">
<TextBlock Text="{DynamicResource Text.Worktree}" Classes="monospace" Margin="4,2" Foreground="#FFDDDDDD"/>
</Border>
</DataTemplate>
</ContentControl.DataTemplates>

View file

@ -17,48 +17,20 @@
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" RowDefinitions="26,*">
<!-- Search -->
<TextBox Grid.Row="0"
Height="26"
BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}"
Background="Transparent"
CornerRadius="4"
Watermark="{DynamicResource Text.CommitDetail.Files.Search}"
Text="{Binding SearchFileFilter, Mode=TwoWay}">
<TextBox.InnerLeftContent>
<Path Width="14" Height="14" Margin="4,0,0,0" Fill="{DynamicResource Brush.FG2}" Data="{StaticResource Icons.Search}"/>
</TextBox.InnerLeftContent>
<TextBox.InnerRightContent>
<Button Classes="icon_button"
IsVisible="{Binding SearchFileFilter, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Command="{Binding ClearSearchFileFilter}">
<Path Width="14" Height="14" Fill="{DynamicResource Brush.FG2}" Data="{StaticResource Icons.Clear}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
<!-- File Tree -->
<Border Grid.Row="1" Margin="0,4,0,0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}">
<TreeDataGrid AutoDragDropRows="False"
ShowColumnHeaders="False"
CanUserResizeColumns="False"
CanUserSortColumns="False"
Source="{Binding RevisionFiles}"
ContextRequested="OnFileContextRequested">
<TreeDataGrid.Resources>
<DataTemplate x:Key="FileTreeNodeExpanderTemplate" DataType="m:FileTreeNode">
<Grid HorizontalAlignment="Stretch" Height="24" ColumnDefinitions="Auto,*">
<Path Grid.Column="0" Classes="folder_icon" Width="14" Height="14" Margin="0,2,0,0" IsVisible="{Binding IsFolder}" Fill="Goldenrod" VerticalAlignment="Center"/>
<Path Grid.Column="0" Width="14" Height="14" IsVisible="{Binding !IsFolder}" Data="{StaticResource Icons.File}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding FullPath, Converter={x:Static c:PathConverters.PureFileName}}" Margin="6,0,0,0"/>
</Grid>
</DataTemplate>
</TreeDataGrid.Resources>
</TreeDataGrid>
</Border>
</Grid>
<!-- File Tree -->
<Border Grid.Column="0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}">
<v:RevisionFileTreeView Revision="{Binding Commit.SHA}" ContextRequested="OnRevisionFileTreeViewContextRequested">
<v:RevisionFileTreeView.Resources>
<DataTemplate x:Key="RevisionFileTreeNodeTemplate" DataType="v:RevisionFileTreeNode">
<Grid HorizontalAlignment="Stretch" Height="24" ColumnDefinitions="Auto,*">
<Path Grid.Column="0" Classes="folder_icon" Width="14" Height="14" Margin="0,2,0,0" IsVisible="{Binding IsFolder}" Fill="Goldenrod" VerticalAlignment="Center"/>
<Path Grid.Column="0" Width="14" Height="14" IsVisible="{Binding !IsFolder}" Data="{StaticResource Icons.File}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding Name}" Margin="6,0,0,0"/>
</Grid>
</DataTemplate>
</v:RevisionFileTreeView.Resources>
</v:RevisionFileTreeView>
</Border>
<GridSplitter Grid.Column="1"
MinWidth="1"

View file

@ -1,8 +1,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Models.TreeDataGrid;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.Imaging;
@ -11,10 +15,155 @@ using Avalonia.Styling;
using AvaloniaEdit;
using AvaloniaEdit.Document;
using AvaloniaEdit.Editing;
using AvaloniaEdit.TextMate;
namespace SourceGit.Views
{
public class RevisionFileTreeNode
{
public Models.Object Backend { get; set; } = null;
public bool IsExpanded { get; set; } = false;
public List<RevisionFileTreeNode> Children { get; set; } = new List<RevisionFileTreeNode>();
public bool IsFolder => Backend != null && Backend.Type == Models.ObjectType.Tree;
public string Name => Backend != null ? Path.GetFileName(Backend.Path) : string.Empty;
}
public class RevisionFileTreeView : UserControl
{
public static readonly StyledProperty<string> RevisionProperty =
AvaloniaProperty.Register<RevisionFileTreeView, string>(nameof(Revision), null);
public string Revision
{
get => GetValue(RevisionProperty);
set => SetValue(RevisionProperty, value);
}
public Models.Object SelectedObject
{
get;
private set;
} = null;
protected override Type StyleKeyOverride => typeof(UserControl);
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == RevisionProperty)
{
SelectedObject = null;
if (Content is TreeDataGrid tree && tree.Source is IDisposable disposable)
disposable.Dispose();
var vm = DataContext as ViewModels.CommitDetail;
if (vm == null)
{
Content = null;
GC.Collect();
return;
}
var objects = vm.GetRevisionFilesUnderFolder(null);
if (objects == null || objects.Count == 0)
{
Content = null;
GC.Collect();
return;
}
var toplevelObjects = new List<RevisionFileTreeNode>();
foreach (var obj in objects)
toplevelObjects.Add(new RevisionFileTreeNode() { Backend = obj });
toplevelObjects.Sort((l, r) =>
{
if (l.IsFolder == r.IsFolder)
return l.Name.CompareTo(r.Name);
return l.IsFolder ? -1 : 1;
});
var template = this.FindResource("RevisionFileTreeNodeTemplate") as IDataTemplate;
var source = new HierarchicalTreeDataGridSource<RevisionFileTreeNode>(toplevelObjects)
{
Columns =
{
new HierarchicalExpanderColumn<RevisionFileTreeNode>(
new TemplateColumn<RevisionFileTreeNode>(null, template, null, GridLength.Auto),
GetChildrenOfTreeNode,
x => x.IsFolder,
x => x.IsExpanded)
}
};
var selection = new Models.TreeDataGridSelectionModel<RevisionFileTreeNode>(source, GetChildrenOfTreeNode);
selection.SingleSelect = true;
selection.SelectionChanged += (s, _) =>
{
if (s is Models.TreeDataGridSelectionModel<RevisionFileTreeNode> model)
{
var node = model.SelectedItem;
var detail = DataContext as ViewModels.CommitDetail;
if (node != null && !node.IsFolder)
{
SelectedObject = node.Backend;
detail.ViewRevisionFile(node.Backend);
}
else
{
SelectedObject = null;
detail.ViewRevisionFile(null);
}
}
};
source.Selection = selection;
Content = new TreeDataGrid()
{
AutoDragDropRows = false,
ShowColumnHeaders = false,
CanUserResizeColumns = false,
CanUserSortColumns = false,
Source = source,
};
GC.Collect();
}
}
private List<RevisionFileTreeNode> GetChildrenOfTreeNode(RevisionFileTreeNode node)
{
if (!node.IsFolder)
return null;
if (node.Children.Count > 0)
return node.Children;
var vm = DataContext as ViewModels.CommitDetail;
if (vm == null)
return null;
var objects = vm.GetRevisionFilesUnderFolder(node.Backend.Path + "/");
if (objects == null || objects.Count == 0)
return null;
foreach (var obj in objects)
node.Children.Add(new RevisionFileTreeNode() { Backend = obj });
node.Children.Sort((l, r) =>
{
if (l.IsFolder == r.IsFolder)
return l.Name.CompareTo(r.Name);
return l.IsFolder ? -1 : 1;
});
return node.Children;
}
}
public class RevisionImageFileView : Control
{
public static readonly StyledProperty<Bitmap> SourceProperty =
@ -59,9 +208,7 @@ namespace SourceGit.Views
var source = Source;
if (source != null)
{
context.DrawImage(source, new Rect(source.Size), new Rect(8, 8, Bounds.Width - 16, Bounds.Height - 16));
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@ -79,9 +226,7 @@ namespace SourceGit.Views
{
var source = Source;
if (source == null)
{
return availableSize;
}
var w = availableSize.Width - 16;
var h = availableSize.Height - 16;
@ -89,13 +234,9 @@ namespace SourceGit.Views
if (size.Width <= w)
{
if (size.Height <= h)
{
return new Size(size.Width + 16, size.Height + 16);
}
else
{
return new Size(h * size.Width / size.Height + 16, availableSize.Height);
}
}
else
{
@ -130,12 +271,6 @@ namespace SourceGit.Views
base.OnLoaded(e);
TextArea.TextView.ContextRequested += OnTextViewContextRequested;
_textMate = Models.TextMateHelper.CreateForEditor(this);
if (DataContext is Models.RevisionTextFile source)
{
Models.TextMateHelper.SetGrammarByFileName(_textMate, source.FileName);
}
}
protected override void OnUnloaded(RoutedEventArgs e)
@ -143,13 +278,6 @@ namespace SourceGit.Views
base.OnUnloaded(e);
TextArea.TextView.ContextRequested -= OnTextViewContextRequested;
if (_textMate != null)
{
_textMate.Dispose();
_textMate = null;
}
GC.Collect();
}
@ -159,20 +287,9 @@ namespace SourceGit.Views
var source = DataContext as Models.RevisionTextFile;
if (source != null)
{
Text = source.Content;
Models.TextMateHelper.SetGrammarByFileName(_textMate, source.FileName);
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property.Name == "ActualThemeVariant" && change.NewValue != null)
{
Models.TextMateHelper.SetThemeByApp(_textMate);
}
else
Text = string.Empty;
}
private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e)
@ -202,8 +319,6 @@ namespace SourceGit.Views
TextArea.TextView.OpenContextMenu(menu);
e.Handled = true;
}
private TextMate.Installation _textMate = null;
}
public partial class RevisionFiles : UserControl
@ -213,15 +328,14 @@ namespace SourceGit.Views
InitializeComponent();
}
private void OnFileContextRequested(object sender, ContextRequestedEventArgs e)
private void OnRevisionFileTreeViewContextRequested(object sender, ContextRequestedEventArgs e)
{
if (DataContext is ViewModels.CommitDetail vm && sender is TreeDataGrid tree)
if (DataContext is ViewModels.CommitDetail vm && sender is RevisionFileTreeView view)
{
var selected = tree.RowSelection.SelectedItem as Models.FileTreeNode;
if (selected != null && !selected.IsFolder && selected.Backend is Models.Object obj)
if (view.SelectedObject != null && view.SelectedObject.Type != Models.ObjectType.Tree)
{
var menu = vm.CreateRevisionFileContextMenu(obj);
tree.OpenContextMenu(menu);
var menu = vm.CreateRevisionFileContextMenu(view.SelectedObject);
view.OpenContextMenu(menu);
}
}

View file

@ -7,31 +7,40 @@
xmlns:v="using:SourceGit.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.TextDiffView"
x:Name="ThisControl"
Background="{DynamicResource Brush.Contents}">
<UserControl.DataTemplates>
<DataTemplate DataType="m:TextDiff">
<v:CombinedTextDiffPresenter HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
BorderBrush="{DynamicResource Brush.Border2}"
<v:CombinedTextDiffPresenter BorderBrush="{DynamicResource Brush.Border2}"
BorderThickness="0"
LineBGEmpty = "{DynamicResource Brush.TextDiffView.LineBG1.EMPTY}"
LineBGAdd = "{DynamicResource Brush.TextDiffView.LineBG1.ADD}"
LineBGDeleted = "{DynamicResource Brush.TextDiffView.LineBG1.DELETED}"
SecondaryLineBGAdd = "{DynamicResource Brush.TextDiffView.LineBG2.ADD}"
SecondaryLineBGDeleted = "{DynamicResource Brush.TextDiffView.LineBG2.DELETED}"
Foreground="{DynamicResource Brush.FG1}"
SecondaryFG="{DynamicResource Brush.FG2}"
FontFamily="{Binding Source={x:Static vm:Preference.Instance}, Path=MonospaceFont}"
DiffData="{Binding}"
SyncScrollOffset="{Binding $parent[v:DiffView].((vm:DiffContext)DataContext).SyncScrollOffset, Mode=TwoWay}"
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting, Mode=TwoWay}"/>
SyncScrollOffset="{Binding #ThisControl.SyncScrollOffset, Mode=TwoWay}"
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap}"/>
</DataTemplate>
<DataTemplate DataType="vm:TwoSideTextDiff">
<Grid ColumnDefinitions="*,1,*">
<v:SingleSideTextDiffPresenter Grid.Column="0"
SyncScrollOffset="{Binding $parent[v:DiffView].((vm:DiffContext)DataContext).SyncScrollOffset, Mode=TwoWay}"
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting, Mode=TwoWay}"
SyncScrollOffset="{Binding #ThisControl.SyncScrollOffset, Mode=TwoWay}"
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap}"
IsOld="True"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
BorderBrush="{DynamicResource Brush.Border2}"
BorderThickness="0"
LineBGEmpty = "{DynamicResource Brush.TextDiffView.LineBG1.EMPTY}"
LineBGAdd = "{DynamicResource Brush.TextDiffView.LineBG1.ADD}"
LineBGDeleted = "{DynamicResource Brush.TextDiffView.LineBG1.DELETED}"
SecondaryLineBGAdd = "{DynamicResource Brush.TextDiffView.LineBG2.ADD}"
SecondaryLineBGDeleted = "{DynamicResource Brush.TextDiffView.LineBG2.DELETED}"
Foreground="{DynamicResource Brush.FG1}"
SecondaryFG="{DynamicResource Brush.FG2}"
FontFamily="{Binding Source={x:Static vm:Preference.Instance}, Path=MonospaceFont}"
@ -40,13 +49,17 @@
<Rectangle Grid.Column="1" Fill="{DynamicResource Brush.Border2}" Width="1" HorizontalAlignment="Center" VerticalAlignment="Stretch"/>
<v:SingleSideTextDiffPresenter Grid.Column="2"
SyncScrollOffset="{Binding $parent[v:DiffView].((vm:DiffContext)DataContext).SyncScrollOffset, Mode=TwoWay}"
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting, Mode=TwoWay}"
SyncScrollOffset="{Binding #ThisControl.SyncScrollOffset, Mode=TwoWay}"
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap}"
IsOld="False"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
BorderBrush="{DynamicResource Brush.Border2}"
BorderThickness="0"
LineBGEmpty = "{DynamicResource Brush.TextDiffView.LineBG1.EMPTY}"
LineBGAdd = "{DynamicResource Brush.TextDiffView.LineBG1.ADD}"
LineBGDeleted = "{DynamicResource Brush.TextDiffView.LineBG1.DELETED}"
SecondaryLineBGAdd = "{DynamicResource Brush.TextDiffView.LineBG2.ADD}"
SecondaryLineBGDeleted = "{DynamicResource Brush.TextDiffView.LineBG2.DELETED}"
Foreground="{DynamicResource Brush.FG1}"
SecondaryFG="{DynamicResource Brush.FG2}"
FontFamily="{Binding Source={x:Static vm:Preference.Instance}, Path=MonospaceFont}"

View file

@ -118,10 +118,6 @@ namespace SourceGit.Views
public class LineBackgroundRenderer : IBackgroundRenderer
{
private static readonly Brush BG_EMPTY = new SolidColorBrush(Color.FromArgb(60, 0, 0, 0));
private static readonly Brush BG_ADDED = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0));
private static readonly Brush BG_DELETED = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0));
public KnownLayer Layer => KnownLayer.Background;
public LineBackgroundRenderer(CombinedTextDiffPresenter editor)
@ -159,11 +155,11 @@ namespace SourceGit.Views
switch (type)
{
case Models.TextDiffLineType.None:
return BG_EMPTY;
return _editor.LineBGEmpty;
case Models.TextDiffLineType.Added:
return BG_ADDED;
return _editor.LineBGAdd;
case Models.TextDiffLineType.Deleted:
return BG_DELETED;
return _editor.LineBGDeleted;
default:
return null;
}
@ -174,9 +170,6 @@ namespace SourceGit.Views
public class LineStyleTransformer : DocumentColorizingTransformer
{
private static readonly Brush HL_ADDED = new SolidColorBrush(Color.FromArgb(90, 0, 255, 0));
private static readonly Brush HL_DELETED = new SolidColorBrush(Color.FromArgb(80, 255, 0, 0));
public LineStyleTransformer(CombinedTextDiffPresenter editor)
{
_editor = editor;
@ -202,7 +195,7 @@ namespace SourceGit.Views
if (info.Highlights.Count > 0)
{
var bg = info.Type == Models.TextDiffLineType.Added ? HL_ADDED : HL_DELETED;
var bg = info.Type == Models.TextDiffLineType.Added ? _editor.SecondaryLineBGAdd : _editor.SecondaryLineBGDeleted;
foreach (var highlight in info.Highlights)
{
ChangeLinePart(line.Offset + highlight.Start, line.Offset + highlight.Start + highlight.Count, v =>
@ -225,6 +218,51 @@ namespace SourceGit.Views
set => SetValue(DiffDataProperty, value);
}
public static readonly StyledProperty<IBrush> LineBGEmptyProperty =
AvaloniaProperty.Register<CombinedTextDiffPresenter, IBrush>(nameof(LineBGEmpty), new SolidColorBrush(Color.FromArgb(60, 0, 0, 0)));
public IBrush LineBGEmpty
{
get => GetValue(LineBGEmptyProperty);
set => SetValue(LineBGEmptyProperty, value);
}
public static readonly StyledProperty<IBrush> LineBGAddProperty =
AvaloniaProperty.Register<CombinedTextDiffPresenter, IBrush>(nameof(LineBGAdd), new SolidColorBrush(Color.FromArgb(60, 0, 255, 0)));
public IBrush LineBGAdd
{
get => GetValue(LineBGAddProperty);
set => SetValue(LineBGAddProperty, value);
}
public static readonly StyledProperty<IBrush> LineBGDeletedProperty =
AvaloniaProperty.Register<CombinedTextDiffPresenter, IBrush>(nameof(LineBGDeleted), new SolidColorBrush(Color.FromArgb(60, 255, 0, 0)));
public IBrush LineBGDeleted
{
get => GetValue(LineBGDeletedProperty);
set => SetValue(LineBGDeletedProperty, value);
}
public static readonly StyledProperty<IBrush> SecondaryLineBGAddProperty =
AvaloniaProperty.Register<CombinedTextDiffPresenter, IBrush>(nameof(SecondaryLineBGAdd), new SolidColorBrush(Color.FromArgb(90, 0, 255, 0)));
public IBrush SecondaryLineBGAdd
{
get => GetValue(SecondaryLineBGAddProperty);
set => SetValue(SecondaryLineBGAddProperty, value);
}
public static readonly StyledProperty<IBrush> SecondaryLineBGDeletedProperty =
AvaloniaProperty.Register<CombinedTextDiffPresenter, IBrush>(nameof(SecondaryLineBGDeleted), new SolidColorBrush(Color.FromArgb(80, 255, 0, 0)));
public IBrush SecondaryLineBGDeleted
{
get => GetValue(SecondaryLineBGDeletedProperty);
set => SetValue(SecondaryLineBGDeletedProperty, value);
}
public static readonly StyledProperty<IBrush> SecondaryFGProperty =
AvaloniaProperty.Register<CombinedTextDiffPresenter, IBrush>(nameof(SecondaryFG), Brushes.Gray);
@ -260,7 +298,6 @@ namespace SourceGit.Views
IsReadOnly = true;
ShowLineNumbers = false;
WordWrap = false;
TextArea.LeftMargins.Add(new LineNumberMargin(this, true) { Margin = new Thickness(8, 0) });
TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this));
@ -500,10 +537,6 @@ namespace SourceGit.Views
public class LineBackgroundRenderer : IBackgroundRenderer
{
private static readonly Brush BG_EMPTY = new SolidColorBrush(Color.FromArgb(60, 0, 0, 0));
private static readonly Brush BG_ADDED = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0));
private static readonly Brush BG_DELETED = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0));
public KnownLayer Layer => KnownLayer.Background;
public LineBackgroundRenderer(SingleSideTextDiffPresenter editor)
@ -542,11 +575,11 @@ namespace SourceGit.Views
switch (type)
{
case Models.TextDiffLineType.None:
return BG_EMPTY;
return _editor.LineBGEmpty;
case Models.TextDiffLineType.Added:
return BG_ADDED;
return _editor.LineBGAdd;
case Models.TextDiffLineType.Deleted:
return BG_DELETED;
return _editor.LineBGDeleted;
default:
return null;
}
@ -557,9 +590,6 @@ namespace SourceGit.Views
public class LineStyleTransformer : DocumentColorizingTransformer
{
private static readonly Brush HL_ADDED = new SolidColorBrush(Color.FromArgb(90, 0, 255, 0));
private static readonly Brush HL_DELETED = new SolidColorBrush(Color.FromArgb(80, 255, 0, 0));
public LineStyleTransformer(SingleSideTextDiffPresenter editor)
{
_editor = editor;
@ -586,7 +616,7 @@ namespace SourceGit.Views
if (info.Highlights.Count > 0)
{
var bg = info.Type == Models.TextDiffLineType.Added ? HL_ADDED : HL_DELETED;
var bg = info.Type == Models.TextDiffLineType.Added ? _editor.LineBGAdd : _editor.LineBGDeleted;
foreach (var highlight in info.Highlights)
{
ChangeLinePart(line.Offset + highlight.Start, line.Offset + highlight.Start + highlight.Count, v =>
@ -618,6 +648,51 @@ namespace SourceGit.Views
set => SetValue(DiffDataProperty, value);
}
public static readonly StyledProperty<IBrush> LineBGEmptyProperty =
AvaloniaProperty.Register<SingleSideTextDiffPresenter, IBrush>(nameof(LineBGEmpty), new SolidColorBrush(Color.FromArgb(60, 0, 0, 0)));
public IBrush LineBGEmpty
{
get => GetValue(LineBGEmptyProperty);
set => SetValue(LineBGEmptyProperty, value);
}
public static readonly StyledProperty<IBrush> LineBGAddProperty =
AvaloniaProperty.Register<SingleSideTextDiffPresenter, IBrush>(nameof(LineBGAdd), new SolidColorBrush(Color.FromArgb(60, 0, 255, 0)));
public IBrush LineBGAdd
{
get => GetValue(LineBGAddProperty);
set => SetValue(LineBGAddProperty, value);
}
public static readonly StyledProperty<IBrush> LineBGDeletedProperty =
AvaloniaProperty.Register<SingleSideTextDiffPresenter, IBrush>(nameof(LineBGDeleted), new SolidColorBrush(Color.FromArgb(60, 255, 0, 0)));
public IBrush LineBGDeleted
{
get => GetValue(LineBGDeletedProperty);
set => SetValue(LineBGDeletedProperty, value);
}
public static readonly StyledProperty<IBrush> SecondaryLineBGAddProperty =
AvaloniaProperty.Register<SingleSideTextDiffPresenter, IBrush>(nameof(SecondaryLineBGAdd), new SolidColorBrush(Color.FromArgb(90, 0, 255, 0)));
public IBrush SecondaryLineBGAdd
{
get => GetValue(SecondaryLineBGAddProperty);
set => SetValue(SecondaryLineBGAddProperty, value);
}
public static readonly StyledProperty<IBrush> SecondaryLineBGDeletedProperty =
AvaloniaProperty.Register<SingleSideTextDiffPresenter, IBrush>(nameof(SecondaryLineBGDeleted), new SolidColorBrush(Color.FromArgb(80, 255, 0, 0)));
public IBrush SecondaryLineBGDeleted
{
get => GetValue(SecondaryLineBGDeletedProperty);
set => SetValue(SecondaryLineBGDeletedProperty, value);
}
public static readonly StyledProperty<IBrush> SecondaryFGProperty =
AvaloniaProperty.Register<SingleSideTextDiffPresenter, IBrush>(nameof(SecondaryFG), Brushes.Gray);
@ -653,7 +728,6 @@ namespace SourceGit.Views
IsReadOnly = true;
ShowLineNumbers = false;
WordWrap = false;
TextArea.LeftMargins.Add(new LineNumberMargin(this) { Margin = new Thickness(8, 0) });
TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this));
@ -842,6 +916,15 @@ namespace SourceGit.Views
set => SetValue(UseSideBySideDiffProperty, value);
}
public static readonly StyledProperty<Vector> SyncScrollOffsetProperty =
AvaloniaProperty.Register<TextDiffView, Vector>(nameof(SyncScrollOffset));
public Vector SyncScrollOffset
{
get => GetValue(SyncScrollOffsetProperty);
set => SetValue(SyncScrollOffsetProperty, value);
}
public TextDiffView()
{
InitializeComponent();
@ -1083,20 +1166,31 @@ namespace SourceGit.Views
{
base.OnPropertyChanged(change);
if (change.Property == TextDiffProperty || change.Property == UseSideBySideDiffProperty)
var data = TextDiff;
if (data == null)
{
if (TextDiff == null)
{
Content = null;
}
else if (UseSideBySideDiff)
{
Content = null;
SyncScrollOffset = Vector.Zero;
return;
}
if (change.Property == TextDiffProperty)
{
if (UseSideBySideDiff)
Content = new ViewModels.TwoSideTextDiff(TextDiff);
}
else
{
Content = TextDiff;
}
SetCurrentValue(SyncScrollOffsetProperty, TextDiff.SyncScrollOffset);
}
else if (change.Property == UseSideBySideDiffProperty)
{
if (UseSideBySideDiff)
Content = new ViewModels.TwoSideTextDiff(TextDiff);
else
Content = TextDiff;
SetCurrentValue(SyncScrollOffsetProperty, Vector.Zero);
}
}

View file

@ -0,0 +1,17 @@
<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:vm="using:SourceGit.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.UpdateSubmodules"
x:DataType="vm:UpdateSubmodules">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.UpdateSubmodules}"/>
<TextBlock Text="{DynamicResource Text.UpdateSubmodules.Tip}"
Margin="0,16,0,0"
HorizontalAlignment="Center"/>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace SourceGit.Views
{
public partial class UpdateSubmodules : UserControl
{
public UpdateSubmodules()
{
InitializeComponent();
}
}
}