feature: add worktree support (#205)

This commit is contained in:
leo 2024-06-27 18:25:16 +08:00
parent 43af8c49a1
commit 8a8aabede3
No known key found for this signature in database
23 changed files with 959 additions and 21 deletions

View file

@ -21,6 +21,7 @@ Opensource Git GUI client.
* Tags * Tags
* Stashes * Stashes
* Submodules * Submodules
* Worktrees
* Archive * Archive
* Diff * Diff
* Save as patch/apply * Save as patch/apply

124
src/Commands/Worktree.cs Normal file
View file

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class Worktree : Command
{
[GeneratedRegex(@"^(\w)\s(\d+)$")]
private static partial Regex REG_AHEAD_BEHIND();
public Worktree(string repo)
{
WorkingDirectory = repo;
Context = repo;
}
public List<Models.Worktree> List()
{
Args = "worktree list --porcelain";
var rs = ReadToEnd();
var worktrees = new List<Models.Worktree>();
var last = null as Models.Worktree;
if (rs.IsSuccess)
{
var lines = rs.StdOut.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.StartsWith("worktree ", StringComparison.Ordinal))
{
last = new Models.Worktree() { FullPath = line.Substring(9).Trim() };
worktrees.Add(last);
}
else if (line.StartsWith("bare", StringComparison.Ordinal))
{
last.IsBare = true;
}
else if (line.StartsWith("HEAD ", StringComparison.Ordinal))
{
last.Head = line.Substring(5).Trim();
}
else if (line.StartsWith("branch ", StringComparison.Ordinal))
{
last.Branch = line.Substring(7).Trim();
}
else if (line.StartsWith("detached", StringComparison.Ordinal))
{
last.IsDetached = true;
}
else if (line.StartsWith("locked", StringComparison.Ordinal))
{
last.IsLocked = true;
}
else if (line.StartsWith("prunable", StringComparison.Ordinal))
{
last.IsPrunable = true;
}
}
}
return worktrees;
}
public bool Add(string fullpath, string name, string tracking, Action<string> outputHandler)
{
Args = "worktree add ";
if (!string.IsNullOrEmpty(tracking))
Args += "--track ";
if (!string.IsNullOrEmpty(name))
Args += $"-b {name} ";
Args += $"\"{fullpath}\" ";
if (!string.IsNullOrEmpty(tracking))
Args += tracking;
_outputHandler = outputHandler;
return Exec();
}
public bool Prune(Action<string> outputHandler)
{
Args = "worktree prune -v";
_outputHandler = outputHandler;
return Exec();
}
public bool Lock(string fullpath, string reason)
{
if (string.IsNullOrEmpty(reason))
Args = $"worktree lock \"{fullpath}\"";
else
Args = $"worktree lock --reason \"{reason}\" \"{fullpath}\"";
return Exec();
}
public bool Unlock(string fullpath)
{
Args = $"worktree unlock \"{fullpath}\"";
return Exec();
}
public bool Remove(string fullpath, bool force, Action<string> outputHandler)
{
if (force)
Args = $"worktree remove -f \"{fullpath}\"";
else
Args = $"worktree remove \"{fullpath}\"";
_outputHandler = outputHandler;
return Exec();
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler = null;
}
}

View file

@ -11,6 +11,7 @@ namespace SourceGit.Models
string GitDir { get; set; } string GitDir { get; set; }
void RefreshBranches(); void RefreshBranches();
void RefreshWorktrees();
void RefreshTags(); void RefreshTags();
void RefreshCommits(); void RefreshCommits();
void RefreshSubmodules(); void RefreshSubmodules();
@ -132,6 +133,7 @@ namespace SourceGit.Models
} }
Task.Run(_repo.RefreshWorkingCopyChanges); Task.Run(_repo.RefreshWorkingCopyChanges);
Task.Run(_repo.RefreshWorktrees);
} }
if (_updateWC > 0 && now > _updateWC) if (_updateWC > 0 && now > _updateWC)

39
src/Models/Worktree.cs Normal file
View file

@ -0,0 +1,39 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.Models
{
public class Worktree : ObservableObject
{
public string Branch { get; set; } = string.Empty;
public string FullPath { get; set; } = string.Empty;
public string Head { get; set; } = string.Empty;
public bool IsBare { get; set; } = false;
public bool IsDetached { get; set; } = false;
public bool IsPrunable { get; set; } = false;
public bool IsLocked
{
get => _isLocked;
set => SetProperty(ref _isLocked, value);
}
public string Name
{
get
{
if (IsDetached)
return $"(deteched HEAD at {Head.Substring(10)})";
if (Branch.StartsWith("refs/heads/", System.StringComparison.Ordinal))
return $"({Branch.Substring(11)})";
if (Branch.StartsWith("refs/remotes/", System.StringComparison.Ordinal))
return $"({Branch.Substring(13)})";
return $"({Branch})";
}
}
private bool _isLocked = false;
}
}

View file

@ -104,4 +104,6 @@
<StreamGeometry x:Key="Icons.Window.Maximize">M153 154h768v768h-768v-768zm64 64v640h640v-640h-640z</StreamGeometry> <StreamGeometry x:Key="Icons.Window.Maximize">M153 154h768v768h-768v-768zm64 64v640h640v-640h-640z</StreamGeometry>
<StreamGeometry x:Key="Icons.Window.Restore">M256 128l0 192L64 320l0 576 704 0 0-192 192 0L960 128 256 128zM704 832 128 832 128 384l576 0L704 832zM896 640l-128 0L768 320 320 320 320 192l576 0L896 640z</StreamGeometry> <StreamGeometry x:Key="Icons.Window.Restore">M256 128l0 192L64 320l0 576 704 0 0-192 192 0L960 128 256 128zM704 832 128 832 128 384l576 0L704 832zM896 640l-128 0L768 320 320 320 320 192l576 0L896 640z</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> <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>
<StreamGeometry x:Key="Icons.Worktree">M512 0C229 0 0 72 0 160v128C0 376 229 448 512 448s512-72 512-160v-128C1024 72 795 0 512 0zM512 544C229 544 0 472 0 384v192c0 88 229 160 512 160s512-72 512-160V384c0 88-229 160-512 160zM512 832c-283 0-512-72-512-160v192C0 952 229 1024 512 1024s512-72 512-160v-192c0 88-229 160-512 160z</StreamGeometry>
<StreamGeometry x:Key="Icons.Worktree.Add">M640 725 768 725 768 597 853 597 853 725 981 725 981 811 853 811 853 939 768 939 768 811 640 811 640 725M384 128C573 128 725 204 725 299 725 393 573 469 384 469 195 469 43 393 43 299 43 204 195 128 384 128M43 384C43 478 195 555 384 555 573 555 725 478 725 384L725 512 683 512 683 595C663 612 640 627 610 640L555 640 555 660C504 675 446 683 384 683 195 683 43 606 43 512L43 384M43 597C43 692 195 768 384 768 446 768 504 760 555 745L555 873C504 888 446 896 384 896 195 896 43 820 43 725L43 597Z</StreamGeometry>
</ResourceDictionary> </ResourceDictionary>

View file

@ -7,6 +7,12 @@
<x:String x:Key="Text.About.Fonts" xml:space="preserve">• Monospace fonts come from </x:String> <x:String x:Key="Text.About.Fonts" xml:space="preserve">• Monospace fonts come from </x:String>
<x:String x:Key="Text.About.SourceCode" xml:space="preserve">• Source code can be found at </x:String> <x:String x:Key="Text.About.SourceCode" xml:space="preserve">• Source code can be found at </x:String>
<x:String x:Key="Text.About.SubTitle" xml:space="preserve">Opensource &amp; Free Git GUI Client</x:String> <x:String x:Key="Text.About.SubTitle" xml:space="preserve">Opensource &amp; Free Git GUI Client</x:String>
<x:String x:Key="Text.AddWorktree" xml:space="preserve">Add Worktree</x:String>
<x:String x:Key="Text.AddWorktree.Location" xml:space="preserve">Location:</x:String>
<x:String x:Key="Text.AddWorktree.Name" xml:space="preserve">Branch Name:</x:String>
<x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">Optional. Default is the destination folder name.</x:String>
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">Track Branch:</x:String>
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">Tracking remote branch</x:String>
<x:String x:Key="Text.Apply" xml:space="preserve">Patch</x:String> <x:String x:Key="Text.Apply" xml:space="preserve">Patch</x:String>
<x:String x:Key="Text.Apply.Error" xml:space="preserve">Error</x:String> <x:String x:Key="Text.Apply.Error" xml:space="preserve">Error</x:String>
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">Raise errors and refuses to apply the patch</x:String> <x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">Raise errors and refuses to apply the patch</x:String>
@ -305,6 +311,10 @@
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">ERROR</x:String> <x:String x:Key="Text.Launcher.Error" xml:space="preserve">ERROR</x:String>
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">NOTICE</x:String> <x:String x:Key="Text.Launcher.Info" xml:space="preserve">NOTICE</x:String>
<x:String x:Key="Text.Launcher.Menu" xml:space="preserve">Open Main Menu</x:String> <x:String x:Key="Text.Launcher.Menu" xml:space="preserve">Open Main Menu</x:String>
<x:String x:Key="Text.LockWorktree" xml:space="preserve">Lock Worktree</x:String>
<x:String x:Key="Text.LockWorktree.Reason" xml:space="preserve">Reason:</x:String>
<x:String x:Key="Text.LockWorktree.Reason.Placeholder" xml:space="preserve">Optional, specify a reason for the lock.</x:String>
<x:String x:Key="Text.LockWorktree.Target" xml:space="preserve">Target:</x:String>
<x:String x:Key="Text.Merge" xml:space="preserve">Merge Branch</x:String> <x:String x:Key="Text.Merge" xml:space="preserve">Merge Branch</x:String>
<x:String x:Key="Text.Merge.Into" xml:space="preserve">Into:</x:String> <x:String x:Key="Text.Merge.Into" xml:space="preserve">Into:</x:String>
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">Merge Option:</x:String> <x:String x:Key="Text.Merge.Mode" xml:space="preserve">Merge Option:</x:String>
@ -367,6 +377,8 @@
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">Tool</x:String> <x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">Tool</x:String>
<x:String x:Key="Text.PruneRemote" xml:space="preserve">Prune Remote</x:String> <x:String x:Key="Text.PruneRemote" xml:space="preserve">Prune Remote</x:String>
<x:String x:Key="Text.PruneRemote.Target" xml:space="preserve">Target:</x:String> <x:String x:Key="Text.PruneRemote.Target" xml:space="preserve">Target:</x:String>
<x:String x:Key="Text.PruneWorktrees" xml:space="preserve">Prune Worktrees</x:String>
<x:String x:Key="Text.PruneWorktrees.Tip" xml:space="preserve">Prune worktree information in `$GIT_DIR/worktrees`</x:String>
<x:String x:Key="Text.Pull" xml:space="preserve">Pull</x:String> <x:String x:Key="Text.Pull" xml:space="preserve">Pull</x:String>
<x:String x:Key="Text.Pull.Branch" xml:space="preserve">Branch:</x:String> <x:String x:Key="Text.Pull.Branch" xml:space="preserve">Branch:</x:String>
<x:String x:Key="Text.Pull.Into" xml:space="preserve">Into:</x:String> <x:String x:Key="Text.Pull.Into" xml:space="preserve">Into:</x:String>
@ -408,6 +420,9 @@
<x:String x:Key="Text.RemoteCM.OpenInBrowser" xml:space="preserve">Open In Browser</x:String> <x:String x:Key="Text.RemoteCM.OpenInBrowser" xml:space="preserve">Open In Browser</x:String>
<x:String x:Key="Text.RemoteCM.Prune" xml:space="preserve">Prune</x:String> <x:String x:Key="Text.RemoteCM.Prune" xml:space="preserve">Prune</x:String>
<x:String x:Key="Text.RemoteCM.Prune.Target" xml:space="preserve">Target:</x:String> <x:String x:Key="Text.RemoteCM.Prune.Target" xml:space="preserve">Target:</x:String>
<x:String x:Key="Text.RemoveWorktree" xml:space="preserve">Confirm to Remove Worktree</x:String>
<x:String x:Key="Text.RemoveWorktree.Force" xml:space="preserve">Enable `--force` Option</x:String>
<x:String x:Key="Text.RemoveWorktree.Target" xml:space="preserve">Target:</x:String>
<x:String x:Key="Text.RenameBranch" xml:space="preserve">Rename Branch</x:String> <x:String x:Key="Text.RenameBranch" xml:space="preserve">Rename Branch</x:String>
<x:String x:Key="Text.RenameBranch.Name" xml:space="preserve">New Name:</x:String> <x:String x:Key="Text.RenameBranch.Name" xml:space="preserve">New Name:</x:String>
<x:String x:Key="Text.RenameBranch.Name.Placeholder" xml:space="preserve">Unique name for this branch</x:String> <x:String x:Key="Text.RenameBranch.Name.Placeholder" xml:space="preserve">Unique name for this branch</x:String>
@ -441,6 +456,9 @@
<x:String x:Key="Text.Repository.Tags" xml:space="preserve">TAGS</x:String> <x:String x:Key="Text.Repository.Tags" xml:space="preserve">TAGS</x:String>
<x:String x:Key="Text.Repository.Tags.Add" xml:space="preserve">NEW TAG</x:String> <x:String x:Key="Text.Repository.Tags.Add" xml:space="preserve">NEW TAG</x:String>
<x:String x:Key="Text.Repository.Terminal" xml:space="preserve">Open In Terminal</x:String> <x:String x:Key="Text.Repository.Terminal" xml:space="preserve">Open In Terminal</x:String>
<x:String x:Key="Text.Repository.Worktrees" xml:space="preserve">WORKTREES</x:String>
<x:String x:Key="Text.Repository.Worktrees.Add" xml:space="preserve">ADD WORKTREE</x:String>
<x:String x:Key="Text.Repository.Worktrees.Prune" xml:space="preserve">PRUNE</x:String>
<x:String x:Key="Text.RepositoryURL" xml:space="preserve">Git Repository URL</x:String> <x:String x:Key="Text.RepositoryURL" xml:space="preserve">Git Repository URL</x:String>
<x:String x:Key="Text.Reset" xml:space="preserve">Reset Current Branch To Revision</x:String> <x:String x:Key="Text.Reset" xml:space="preserve">Reset Current Branch To Revision</x:String>
<x:String x:Key="Text.Reset.Mode" xml:space="preserve">Reset Mode:</x:String> <x:String x:Key="Text.Reset.Mode" xml:space="preserve">Reset Mode:</x:String>
@ -541,4 +559,8 @@
<x:String x:Key="Text.WorkingCopy.Unstaged.ViewAssumeUnchaged" xml:space="preserve">VIEW ASSUME UNCHANGED</x:String> <x:String x:Key="Text.WorkingCopy.Unstaged.ViewAssumeUnchaged" xml:space="preserve">VIEW ASSUME UNCHANGED</x:String>
<x:String x:Key="Text.WorkingCopy.ResolveTip" xml:space="preserve">Right-click the selected file(s), and make your choice to resolve conflicts.</x:String> <x:String x:Key="Text.WorkingCopy.ResolveTip" xml:space="preserve">Right-click the selected file(s), and make your choice to resolve conflicts.</x:String>
<x:String x:Key="Text.Worktree" xml:space="preserve">WORKTREE</x:String> <x:String x:Key="Text.Worktree" xml:space="preserve">WORKTREE</x:String>
<x:String x:Key="Text.Worktree.CopyPath" xml:space="preserve">Copy Path</x:String>
<x:String x:Key="Text.Worktree.Lock" xml:space="preserve">Lock</x:String>
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">Remove</x:String>
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">Unlock</x:String>
</ResourceDictionary> </ResourceDictionary>

View file

@ -10,6 +10,12 @@
<x:String x:Key="Text.About.Fonts" 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.SourceCode" xml:space="preserve">• 项目源代码地址 </x:String>
<x:String x:Key="Text.About.SubTitle" xml:space="preserve">开源免费的Git客户端</x:String> <x:String x:Key="Text.About.SubTitle" xml:space="preserve">开源免费的Git客户端</x:String>
<x:String x:Key="Text.AddWorktree" xml:space="preserve">新增工作树</x:String>
<x:String x:Key="Text.AddWorktree.Location" xml:space="preserve">工作树路径 </x:String>
<x:String x:Key="Text.AddWorktree.Name" xml:space="preserve">自定义分支名 </x:String>
<x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">选填。默认使用目标文件夹名称。</x:String>
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">跟踪分支</x:String>
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">设置上游跟踪分支</x:String>
<x:String x:Key="Text.Apply" xml:space="preserve">应用补丁(apply)</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" xml:space="preserve">错误</x:String>
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">输出错误,并终止应用补丁</x:String> <x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">输出错误,并终止应用补丁</x:String>
@ -308,6 +314,10 @@
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">出错了</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.Info" xml:space="preserve">系统提示</x:String>
<x:String x:Key="Text.Launcher.Menu" xml:space="preserve">主菜单</x:String> <x:String x:Key="Text.Launcher.Menu" xml:space="preserve">主菜单</x:String>
<x:String x:Key="Text.LockWorktree" xml:space="preserve">锁定工作树</x:String>
<x:String x:Key="Text.LockWorktree.Reason" xml:space="preserve">原因 </x:String>
<x:String x:Key="Text.LockWorktree.Reason.Placeholder" xml:space="preserve">选填,为此锁定操作描述原因。</x:String>
<x:String x:Key="Text.LockWorktree.Target" xml:space="preserve">目标工作树 </x:String>
<x:String x:Key="Text.Merge" 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.Into" xml:space="preserve">目标分支 </x:String>
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">合并方式 </x:String> <x:String x:Key="Text.Merge.Mode" xml:space="preserve">合并方式 </x:String>
@ -370,6 +380,8 @@
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">工具</x:String> <x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">工具</x:String>
<x:String x:Key="Text.PruneRemote" xml:space="preserve">清理远程已删除分支</x:String> <x:String x:Key="Text.PruneRemote" xml:space="preserve">清理远程已删除分支</x:String>
<x:String x:Key="Text.PruneRemote.Target" xml:space="preserve">目标 </x:String> <x:String x:Key="Text.PruneRemote.Target" xml:space="preserve">目标 </x:String>
<x:String x:Key="Text.PruneWorktrees" xml:space="preserve">清理工作树</x:String>
<x:String x:Key="Text.PruneWorktrees.Tip" xml:space="preserve">清理在`$GIT_DIR/worktrees`中的无效工作树信息</x:String>
<x:String x:Key="Text.Pull" xml:space="preserve">拉回(pull)</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.Branch" xml:space="preserve">拉取分支 </x:String>
<x:String x:Key="Text.Pull.Into" xml:space="preserve">本地分支 </x:String> <x:String x:Key="Text.Pull.Into" xml:space="preserve">本地分支 </x:String>
@ -410,6 +422,9 @@
<x:String x:Key="Text.RemoteCM.Fetch" xml:space="preserve">拉取(fetch)更新</x:String> <x:String x:Key="Text.RemoteCM.Fetch" xml:space="preserve">拉取(fetch)更新</x:String>
<x:String x:Key="Text.RemoteCM.OpenInBrowser" xml:space="preserve">在浏览器中打开</x:String> <x:String x:Key="Text.RemoteCM.OpenInBrowser" xml:space="preserve">在浏览器中打开</x:String>
<x:String x:Key="Text.RemoteCM.Prune" xml:space="preserve">清理远程已删除分支</x:String> <x:String x:Key="Text.RemoteCM.Prune" xml:space="preserve">清理远程已删除分支</x:String>
<x:String x:Key="Text.RemoveWorktree" xml:space="preserve">移除工作树操作确认</x:String>
<x:String x:Key="Text.RemoveWorktree.Force" xml:space="preserve">启用`--force`选项</x:String>
<x:String x:Key="Text.RemoveWorktree.Target" xml:space="preserve">目标工作树 </x:String>
<x:String x:Key="Text.RenameBranch" 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" xml:space="preserve">新的名称 </x:String>
<x:String x:Key="Text.RenameBranch.Name.Placeholder" xml:space="preserve">新的分支名不能与现有分支名相同</x:String> <x:String x:Key="Text.RenameBranch.Name.Placeholder" xml:space="preserve">新的分支名不能与现有分支名相同</x:String>
@ -443,6 +458,9 @@
<x:String x:Key="Text.Repository.Tags" 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.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.Terminal" xml:space="preserve">在终端中打开</x:String>
<x:String x:Key="Text.Repository.Worktrees" xml:space="preserve">工作树列表</x:String>
<x:String x:Key="Text.Repository.Worktrees.Add" xml:space="preserve">新增工作树</x:String>
<x:String x:Key="Text.Repository.Worktrees.Prune" xml:space="preserve">清理</x:String>
<x:String x:Key="Text.RepositoryURL" 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" xml:space="preserve">重置(reset)当前分支到指定版本</x:String>
<x:String x:Key="Text.Reset.Mode" xml:space="preserve">重置模式 </x:String> <x:String x:Key="Text.Reset.Mode" xml:space="preserve">重置模式 </x:String>
@ -543,4 +561,8 @@
<x:String x:Key="Text.WorkingCopy.Unstaged.ViewAssumeUnchaged" 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.WorkingCopy.ResolveTip" xml:space="preserve">请选中冲突文件,打开右键菜单,选择合适的解决方式</x:String>
<x:String x:Key="Text.Worktree" xml:space="preserve">本地工作树</x:String> <x:String x:Key="Text.Worktree" xml:space="preserve">本地工作树</x:String>
<x:String x:Key="Text.Worktree.CopyPath" xml:space="preserve">复制工作树路径</x:String>
<x:String x:Key="Text.Worktree.Lock" xml:space="preserve">锁定工作树</x:String>
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">移除工作树</x:String>
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">解除工作树锁定</x:String>
</ResourceDictionary> </ResourceDictionary>

View file

@ -10,6 +10,12 @@
<x:String x:Key="Text.About.Fonts" 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.SourceCode" xml:space="preserve">• 專案原始碼地址 </x:String>
<x:String x:Key="Text.About.SubTitle" xml:space="preserve">開源免費的Git客戶端</x:String> <x:String x:Key="Text.About.SubTitle" xml:space="preserve">開源免費的Git客戶端</x:String>
<x:String x:Key="Text.AddWorktree" xml:space="preserve">新增工作樹</x:String>
<x:String x:Key="Text.AddWorktree.Location" xml:space="preserve">工作樹路徑 </x:String>
<x:String x:Key="Text.AddWorktree.Name" xml:space="preserve">自定义分支名 </x:String>
<x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">選填。 預設使用目標資料夾名稱。</x:String>
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">跟蹤分支</x:String>
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">設置上游跟蹤分支</x:String>
<x:String x:Key="Text.Apply" xml:space="preserve">應用補丁(apply)</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" xml:space="preserve">錯誤</x:String>
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">輸出錯誤,並終止應用補丁</x:String> <x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">輸出錯誤,並終止應用補丁</x:String>
@ -308,6 +314,10 @@
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">出錯了</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.Info" xml:space="preserve">系統提示</x:String>
<x:String x:Key="Text.Launcher.Menu" xml:space="preserve">主選單</x:String> <x:String x:Key="Text.Launcher.Menu" xml:space="preserve">主選單</x:String>
<x:String x:Key="Text.LockWorktree" xml:space="preserve">鎖定工作樹</x:String>
<x:String x:Key="Text.LockWorktree.Reason" xml:space="preserve">原因 </x:String>
<x:String x:Key="Text.LockWorktree.Reason.Placeholder" xml:space="preserve">選填,為此鎖定操作描述原因。</x:String>
<x:String x:Key="Text.LockWorktree.Target" xml:space="preserve">目標工作樹 </x:String>
<x:String x:Key="Text.Merge" 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.Into" xml:space="preserve">目標分支 </x:String>
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">合併方式 </x:String> <x:String x:Key="Text.Merge.Mode" xml:space="preserve">合併方式 </x:String>
@ -370,6 +380,8 @@
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">工具</x:String> <x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">工具</x:String>
<x:String x:Key="Text.PruneRemote" xml:space="preserve">清理遠端已刪除分支</x:String> <x:String x:Key="Text.PruneRemote" xml:space="preserve">清理遠端已刪除分支</x:String>
<x:String x:Key="Text.PruneRemote.Target" xml:space="preserve">目標 </x:String> <x:String x:Key="Text.PruneRemote.Target" xml:space="preserve">目標 </x:String>
<x:String x:Key="Text.PruneWorktrees" xml:space="preserve">清理工作樹</x:String>
<x:String x:Key="Text.PruneWorktrees.Tip" xml:space="preserve">清理在`$GIT_DIR/worktrees`中的無效工作樹資訊</x:String>
<x:String x:Key="Text.Pull" xml:space="preserve">拉回(pull)</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.Branch" xml:space="preserve">拉取分支 </x:String>
<x:String x:Key="Text.Pull.Into" xml:space="preserve">本地分支 </x:String> <x:String x:Key="Text.Pull.Into" xml:space="preserve">本地分支 </x:String>
@ -410,6 +422,9 @@
<x:String x:Key="Text.RemoteCM.Fetch" xml:space="preserve">拉取(fetch)更新</x:String> <x:String x:Key="Text.RemoteCM.Fetch" xml:space="preserve">拉取(fetch)更新</x:String>
<x:String x:Key="Text.RemoteCM.OpenInBrowser" xml:space="preserve">在瀏覽器中訪問網址</x:String> <x:String x:Key="Text.RemoteCM.OpenInBrowser" xml:space="preserve">在瀏覽器中訪問網址</x:String>
<x:String x:Key="Text.RemoteCM.Prune" xml:space="preserve">清理遠端已刪除分支</x:String> <x:String x:Key="Text.RemoteCM.Prune" xml:space="preserve">清理遠端已刪除分支</x:String>
<x:String x:Key="Text.RemoveWorktree" xml:space="preserve">刪除工作樹操作確認</x:String>
<x:String x:Key="Text.RemoveWorktree.Force" xml:space="preserve">啟用`--force`選項</x:String>
<x:String x:Key="Text.RemoveWorktree.Target" xml:space="preserve">目標工作樹 </x:String>
<x:String x:Key="Text.RenameBranch" 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" xml:space="preserve">新的名稱 </x:String>
<x:String x:Key="Text.RenameBranch.Name.Placeholder" xml:space="preserve">新的分支名不能與現有分支名相同</x:String> <x:String x:Key="Text.RenameBranch.Name.Placeholder" xml:space="preserve">新的分支名不能與現有分支名相同</x:String>
@ -443,6 +458,9 @@
<x:String x:Key="Text.Repository.Tags" 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.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.Terminal" xml:space="preserve">在終端中開啟</x:String>
<x:String x:Key="Text.Repository.Worktrees" xml:space="preserve">工作樹清單</x:String>
<x:String x:Key="Text.Repository.Worktrees.Add" xml:space="preserve">新建工作樹</x:String>
<x:String x:Key="Text.Repository.Worktrees.Prune" xml:space="preserve">清理</x:String>
<x:String x:Key="Text.RepositoryURL" 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" xml:space="preserve">重置(reset)當前分支到指定版本</x:String>
<x:String x:Key="Text.Reset.Mode" xml:space="preserve">重置模式 </x:String> <x:String x:Key="Text.Reset.Mode" xml:space="preserve">重置模式 </x:String>
@ -543,4 +561,8 @@
<x:String x:Key="Text.WorkingCopy.Unstaged.ViewAssumeUnchaged" 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.WorkingCopy.ResolveTip" xml:space="preserve">請選中衝突檔案,開啟右鍵選單,選擇合適的解決方式</x:String>
<x:String x:Key="Text.Worktree" xml:space="preserve">本地工作樹</x:String> <x:String x:Key="Text.Worktree" xml:space="preserve">本地工作樹</x:String>
<x:String x:Key="Text.Worktree.CopyPath" xml:space="preserve">拷贝工作樹路徑</x:String>
<x:String x:Key="Text.Worktree.Lock" xml:space="preserve">鎖定工作樹</x:String>
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">移除工作樹</x:String>
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">解除鎖定工作樹</x:String>
</ResourceDictionary> </ResourceDictionary>

View file

@ -0,0 +1,122 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace SourceGit.ViewModels
{
public partial class AddWorktree : Popup
{
[GeneratedRegex(@"^[\w\-/\.]+$")]
private static partial Regex REG_NAME();
[Required(ErrorMessage = "Worktree path is required!")]
[CustomValidation(typeof(AddWorktree), nameof(ValidateWorktreePath))]
public string FullPath
{
get => _fullPath;
set => SetProperty(ref _fullPath, value, true);
}
[CustomValidation(typeof(AddWorktree), nameof(ValidateBranchName))]
public string CustomName
{
get => _customName;
set => SetProperty(ref _customName, value, true);
}
public bool SetTrackingBranch
{
get => _setTrackingBranch;
set => SetProperty(ref _setTrackingBranch, value);
}
public List<string> TrackingBranches
{
get;
private set;
}
public string SelectedTrackingBranch
{
get;
set;
}
public AddWorktree(Repository repo)
{
_repo = repo;
TrackingBranches = new List<string>();
foreach (var branch in repo.Branches)
{
if (!branch.IsLocal)
TrackingBranches.Add($"{branch.Remote}/{branch.Name}");
}
if (TrackingBranches.Count > 0)
SelectedTrackingBranch = TrackingBranches[0];
else
SelectedTrackingBranch = string.Empty;
View = new Views.AddWorktree() { DataContext = this };
}
public static ValidationResult ValidateWorktreePath(string folder, ValidationContext _)
{
var info = new DirectoryInfo(folder);
if (info.Exists)
{
var files = info.GetFiles();
if (files.Length > 0)
return new ValidationResult("Given path is not empty!!!");
var folders = info.GetDirectories();
if (folders.Length > 0)
return new ValidationResult("Given path is not empty!!!");
}
return ValidationResult.Success;
}
public static ValidationResult ValidateBranchName(string name, ValidationContext ctx)
{
if (string.IsNullOrEmpty(name))
return ValidationResult.Success;
var creator = ctx.ObjectInstance as AddWorktree;
if (creator == null)
return new ValidationResult("Missing runtime context to create branch!");
foreach (var b in creator._repo.Branches)
{
var test = b.IsLocal ? b.Name : $"{b.Remote}/{b.Name}";
if (test == name)
return new ValidationResult("A branch with same name already exists!");
}
return ValidationResult.Success;
}
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Adding worktree ...";
var tracking = _setTrackingBranch ? SelectedTrackingBranch : string.Empty;
return Task.Run(() =>
{
var succ = new Commands.Worktree(_repo.FullPath).Add(_fullPath, _customName, tracking, SetProgressDescription);
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
private string _fullPath = string.Empty;
private string _customName = string.Empty;
private bool _setTrackingBranch = false;
}
}

View file

@ -0,0 +1,44 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels
{
public class LockWorktree : Popup
{
public Models.Worktree Target
{
get;
private set;
} = null;
public string Reason
{
get;
set;
} = string.Empty;
public LockWorktree(Repository repo, Models.Worktree target)
{
_repo = repo;
Target = target;
View = new Views.LockWorktree() { DataContext = this };
}
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Locking worktrees ...";
return Task.Run(() =>
{
var succ = new Commands.Worktree(_repo.FullPath).Lock(Target.FullPath, Reason);
if (succ)
Target.IsLocked = true;
CallUIThread(() => _repo.SetWatcherEnabled(true));
return true;
});
}
private Repository _repo = null;
}
}

View file

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

View file

@ -0,0 +1,41 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels
{
public class RemoveWorktree : Popup
{
public Models.Worktree Target
{
get;
private set;
} = null;
public bool Force
{
get;
set;
} = false;
public RemoveWorktree(Repository repo, Models.Worktree target)
{
_repo = repo;
Target = target;
View = new Views.RemoveWorktree() { DataContext = this };
}
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Remove worktrees ...";
return Task.Run(() =>
{
var succ = new Commands.Worktree(_repo.FullPath).Remove(Target.FullPath, Force, SetProgressDescription);
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
}
}

View file

@ -131,6 +131,13 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _remoteBranchTrees, value); private set => SetProperty(ref _remoteBranchTrees, value);
} }
[JsonIgnore]
public List<Models.Worktree> Worktrees
{
get => _worktrees;
private set => SetProperty(ref _worktrees, value);
}
[JsonIgnore] [JsonIgnore]
public List<Models.Tag> Tags public List<Models.Tag> Tags
{ {
@ -219,6 +226,13 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _isSubmoduleGroupExpanded, value); set => SetProperty(ref _isSubmoduleGroupExpanded, value);
} }
[JsonIgnore]
public bool IsWorktreeGroupExpanded
{
get => _isWorktreeGroupExpanded;
set => SetProperty(ref _isWorktreeGroupExpanded, value);
}
[JsonIgnore] [JsonIgnore]
public InProgressContext InProgressContext public InProgressContext InProgressContext
{ {
@ -295,6 +309,7 @@ namespace SourceGit.ViewModels
}); });
Task.Run(RefreshSubmodules); Task.Run(RefreshSubmodules);
Task.Run(RefreshWorktrees);
Task.Run(RefreshWorkingCopyChanges); Task.Run(RefreshWorkingCopyChanges);
Task.Run(RefreshStashes); Task.Run(RefreshStashes);
} }
@ -590,11 +605,31 @@ namespace SourceGit.ViewModels
}); });
} }
public void RefreshWorktrees()
{
var worktrees = new Commands.Worktree(_fullpath).List();
var cleaned = new List<Models.Worktree>();
foreach (var worktree in worktrees)
{
if (worktree.IsBare || worktree.FullPath.Equals(_fullpath))
continue;
cleaned.Add(worktree);
}
Dispatcher.UIThread.Invoke(() =>
{
Worktrees = cleaned;
});
}
public void RefreshTags() public void RefreshTags()
{ {
var tags = new Commands.QueryTags(FullPath).Result(); var tags = new Commands.QueryTags(FullPath).Result();
foreach (var tag in tags) foreach (var tag in tags)
tag.IsFiltered = Filters.Contains(tag.Name); tag.IsFiltered = Filters.Contains(tag.Name);
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.Invoke(() =>
{ {
Tags = tags; Tags = tags;
@ -656,10 +691,7 @@ namespace SourceGit.ViewModels
public void RefreshSubmodules() public void RefreshSubmodules()
{ {
var submodules = new Commands.QuerySubmodules(FullPath).Result(); var submodules = new Commands.QuerySubmodules(FullPath).Result();
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.Invoke(() => Submodules = submodules);
{
Submodules = submodules;
});
} }
public void RefreshWorkingCopyChanges() public void RefreshWorkingCopyChanges()
@ -732,6 +764,16 @@ namespace SourceGit.ViewModels
public void CheckoutBranch(Models.Branch branch) public void CheckoutBranch(Models.Branch branch)
{ {
if (branch.IsLocal)
{
var worktree = _worktrees.Find(x => x.Branch == branch.FullName);
if (worktree != null)
{
OpenWorktree(worktree);
return;
}
}
if (!PopupHost.CanCreatePopup()) if (!PopupHost.CanCreatePopup())
return; return;
@ -817,6 +859,36 @@ namespace SourceGit.ViewModels
} }
} }
public void AddWorktree()
{
if (PopupHost.CanCreatePopup())
PopupHost.ShowPopup(new AddWorktree(this));
}
public void PruneWorktrees()
{
if (PopupHost.CanCreatePopup())
PopupHost.ShowAndStartPopup(new PruneWorktrees(this));
}
public void OpenWorktree(Models.Worktree worktree)
{
var gitDir = new Commands.QueryGitDir(worktree.FullPath).Result();
var repo = Preference.AddRepository(worktree.FullPath, gitDir);
var node = new RepositoryNode()
{
Id = repo.FullPath,
Name = Path.GetFileName(repo.FullPath),
Bookmark = 0,
IsRepository = true,
};
var launcher = App.GetTopLevel().DataContext as Launcher;
if (launcher != null)
launcher.OpenRepositoryInTab(node, null);
}
public ContextMenu CreateContextMenuForGitFlow() public ContextMenu CreateContextMenuForGitFlow()
{ {
var menu = new ContextMenu(); var menu = new ContextMenu();
@ -1260,9 +1332,8 @@ namespace SourceGit.ViewModels
target.Click += (o, e) => target.Click += (o, e) =>
{ {
if (Commands.Branch.SetUpstream(_fullpath, branch.Name, upstream)) if (Commands.Branch.SetUpstream(_fullpath, branch.Name, upstream))
{
Task.Run(RefreshBranches); Task.Run(RefreshBranches);
}
e.Handled = true; e.Handled = true;
}; };
@ -1274,9 +1345,8 @@ namespace SourceGit.ViewModels
unsetUpstream.Click += (_, e) => unsetUpstream.Click += (_, e) =>
{ {
if (Commands.Branch.SetUpstream(_fullpath, branch.Name, string.Empty)) if (Commands.Branch.SetUpstream(_fullpath, branch.Name, string.Empty))
{
Task.Run(RefreshBranches); Task.Run(RefreshBranches);
}
e.Handled = true; e.Handled = true;
}; };
tracking.Items.Add(new MenuItem() { Header = "-" }); tracking.Items.Add(new MenuItem() { Header = "-" });
@ -1634,6 +1704,65 @@ namespace SourceGit.ViewModels
return menu; return menu;
} }
public ContextMenu CreateContextMenuForWorktree(Models.Worktree worktree)
{
var menu = new ContextMenu();
if (worktree.IsLocked)
{
var unlock = new MenuItem();
unlock.Header = App.Text("Worktree.Unlock");
unlock.Icon = App.CreateMenuIcon("Icons.Unlock");
unlock.Click += (o, ev) =>
{
SetWatcherEnabled(false);
var succ = new Commands.Worktree(_fullpath).Unlock(worktree.FullPath);
if (succ)
worktree.IsLocked = false;
SetWatcherEnabled(true);
ev.Handled = true;
};
menu.Items.Add(unlock);
}
else
{
var loc = new MenuItem();
loc.Header = App.Text("Worktree.Lock");
loc.Icon = App.CreateMenuIcon("Icons.Lock");
loc.Click += (o, ev) =>
{
if (PopupHost.CanCreatePopup())
PopupHost.ShowPopup(new LockWorktree(this, worktree));
ev.Handled = true;
};
menu.Items.Add(loc);
}
var remove = new MenuItem();
remove.Header = App.Text("Worktree.Remove");
remove.Icon = App.CreateMenuIcon("Icons.Clear");
remove.Click += (o, ev) =>
{
if (PopupHost.CanCreatePopup())
PopupHost.ShowPopup(new RemoveWorktree(this, worktree));
ev.Handled = true;
};
menu.Items.Add(remove);
var copy = new MenuItem();
copy.Header = App.Text("Worktree.CopyPath");
copy.Icon = App.CreateMenuIcon("Icons.Copy");
copy.Click += (o, e) =>
{
App.CopyText(worktree.FullPath);
e.Handled = true;
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(copy);
return menu;
}
private MenuItem CreateMenuItemToCompareBranches(Models.Branch branch) private MenuItem CreateMenuItemToCompareBranches(Models.Branch branch)
{ {
if (Branches.Count == 1) if (Branches.Count == 1)
@ -1712,6 +1841,7 @@ namespace SourceGit.ViewModels
private bool _isTagGroupExpanded = false; private bool _isTagGroupExpanded = false;
private bool _isSubmoduleGroupExpanded = false; private bool _isSubmoduleGroupExpanded = false;
private bool _isWorktreeGroupExpanded = false;
private string _searchBranchFilter = string.Empty; private string _searchBranchFilter = string.Empty;
@ -1719,6 +1849,7 @@ namespace SourceGit.ViewModels
private List<Models.Branch> _branches = new List<Models.Branch>(); private List<Models.Branch> _branches = new List<Models.Branch>();
private List<BranchTreeNode> _localBranchTrees = new List<BranchTreeNode>(); private List<BranchTreeNode> _localBranchTrees = new List<BranchTreeNode>();
private List<BranchTreeNode> _remoteBranchTrees = new List<BranchTreeNode>(); private List<BranchTreeNode> _remoteBranchTrees = new List<BranchTreeNode>();
private List<Models.Worktree> _worktrees = new List<Models.Worktree>();
private List<Models.Tag> _tags = new List<Models.Tag>(); private List<Models.Tag> _tags = new List<Models.Tag>();
private List<string> _submodules = new List<string>(); private List<string> _submodules = new List<string>();
private bool _includeUntracked = true; private bool _includeUntracked = true;

View file

@ -0,0 +1,71 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:SourceGit.Models"
xmlns:v="using:SourceGit.Views"
xmlns:c="using:SourceGit.Converters"
xmlns:vm="using:SourceGit.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.AddWorktree"
x:DataType="vm:AddWorktree">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.AddWorktree}"/>
<Grid Margin="0,16,0,0" RowDefinitions="32,32,32,Auto" ColumnDefinitions="150,*">
<TextBlock Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.AddWorktree.Location}"/>
<TextBox Grid.Row="0" Grid.Column="1"
x:Name="TxtLocation"
Height="28"
CornerRadius="3"
Text="{Binding FullPath, Mode=TwoWay}">
<TextBox.InnerRightContent>
<Button Classes="icon_button" Width="28" Height="28" Margin="4,0,0,0" Click="SelectLocation">
<Path Data="{StaticResource Icons.Folder.Open}" Fill="{DynamicResource Brush.FG1}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
<TextBlock Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.AddWorktree.Name}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="28"
CornerRadius="3"
Text="{Binding CustomName, Mode=TwoWay}"
Watermark="{DynamicResource Text.AddWorktree.Name.Placeholder}"/>
<CheckBox Grid.Row="2" Grid.Column="1"
Content="{DynamicResource Text.AddWorktree.Tracking.Toggle}"
IsChecked="{Binding SetTrackingBranch, Mode=TwoWay}"/>
<Border Grid.Row="3" Grid.Column="0"
Height="32"
IsVisible="{Binding SetTrackingBranch, Mode=OneWay}">
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.AddWorktree.Tracking}"/>
</Border>
<ComboBox Grid.Row="3" Grid.Column="1"
Height="28" Padding="8,0"
VerticalAlignment="Center" HorizontalAlignment="Stretch"
ItemsSource="{Binding TrackingBranches}"
SelectedItem="{Binding SelectedTrackingBranch, Mode=TwoWay}"
IsVisible="{Binding SetTrackingBranch, Mode=OneWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Height="20" VerticalAlignment="Center">
<Path Margin="0,0,8,0" Width="14" Height="14" Fill="{DynamicResource Brush.FG1}" Data="{StaticResource Icons.Branch}"/>
<TextBlock Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,28 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
namespace SourceGit.Views
{
public partial class AddWorktree : UserControl
{
public AddWorktree()
{
InitializeComponent();
}
private async void SelectLocation(object sender, RoutedEventArgs e)
{
var options = new FolderPickerOpenOptions() { AllowMultiple = false };
var toplevel = TopLevel.GetTopLevel(this);
if (toplevel == null)
return;
var selected = await toplevel.StorageProvider.OpenFolderPickerAsync(options);
if (selected.Count == 1)
TxtLocation.Text = selected[0].Path.LocalPath;
e.Handled = true;
}
}
}

View file

@ -0,0 +1,41 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:SourceGit.Models"
xmlns:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.LockWorktree"
x:DataType="vm:LockWorktree">
<StackPanel Orientation="Vertical" Margin="8,0,0,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.LockWorktree}"/>
<Grid Margin="8,16,0,0" RowDefinitions="32,32" ColumnDefinitions="120,*">
<TextBlock Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right"
Margin="8,0"
Text="{DynamicResource Text.LockWorktree.Target}"/>
<Grid Grid.Row="0" Grid.Column="1" ColumnDefinitions="Auto,*">
<Path Grid.Column="0" Width="12" Height="12" Data="{StaticResource Icons.Worktree}"/>
<TextBlock Grid.Column="1" Classes="monospace" Margin="8,0,0,0" TextTrimming="CharacterEllipsis">
<Run Text="{Binding Target.FullPath}"/>
<Run Text="{Binding Target.Name}" Foreground="{DynamicResource Brush.FG2}"/>
</TextBlock>
</Grid>
<TextBlock Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Right"
Margin="8,0"
Text="{DynamicResource Text.LockWorktree.Reason}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="26"
CornerRadius="3"
Text="{Binding Reason, Mode=TwoWay}"
Watermark="{DynamicResource Text.LockWorktree.Reason.Placeholder}"
v:AutoFocusBehaviour.IsEnabled="True"/>
</Grid>
</StackPanel>
</UserControl>

View file

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

View file

@ -0,0 +1,15 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.PruneWorktrees">
<StackPanel Orientation="Vertical" Margin="8,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.PruneWorktrees}"/>
<TextBlock Text="{DynamicResource Text.PruneWorktrees.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 PruneWorktrees : UserControl
{
public PruneWorktrees()
{
InitializeComponent();
}
}
}

View file

@ -0,0 +1,34 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:SourceGit.Models"
xmlns:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.RemoveWorktree"
x:DataType="vm:RemoveWorktree">
<StackPanel Orientation="Vertical" Margin="8,0,0,0">
<TextBlock FontSize="18"
Classes="bold"
Text="{DynamicResource Text.RemoveWorktree}"/>
<Grid Margin="8,16,0,0" RowDefinitions="32,32" ColumnDefinitions="120,*">
<TextBlock Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right"
Margin="8,0"
Text="{DynamicResource Text.RemoveWorktree.Target}"/>
<Grid Grid.Row="0" Grid.Column="1" ColumnDefinitions="Auto,*">
<Path Grid.Column="0" Width="12" Height="12" Data="{StaticResource Icons.Worktree}"/>
<TextBlock Grid.Column="1" Classes="monospace" Margin="8,0,0,0" TextTrimming="CharacterEllipsis">
<Run Text="{Binding Target.FullPath}"/>
<Run Text="{Binding Target.Name}" Foreground="{DynamicResource Brush.FG2}"/>
</TextBlock>
</Grid>
<CheckBox Grid.Row="1" Grid.Column="1"
Content="{DynamicResource Text.RemoveWorktree.Force}"
IsChecked="{Binding Force, Mode=TwoWay}"/>
</Grid>
</StackPanel>
</UserControl>

View file

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

View file

@ -119,7 +119,7 @@
</Grid> </Grid>
<!-- Dashboard --> <!-- Dashboard -->
<Grid Grid.Row="1" Margin="0,0,0,8" RowDefinitions="Auto,Auto,28,Auto,28,*,28,Auto,28,Auto" IsVisible="{Binding !IsSearching}"> <Grid Grid.Row="1" Margin="0,0,0,8" RowDefinitions="Auto,Auto,28,Auto,28,*,28,Auto,28,Auto,28,Auto" IsVisible="{Binding !IsSearching}">
<!-- Page Switcher for Right Panel --> <!-- Page Switcher for Right Panel -->
<Border Grid.Row="0" Margin="8,0,4,0" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}" CornerRadius="6"> <Border Grid.Row="0" Margin="8,0,4,0" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}" CornerRadius="6">
<Border CornerRadius="6" ClipToBounds="True"> <Border CornerRadius="6" ClipToBounds="True">
@ -463,7 +463,7 @@
Command="{Binding UpdateSubmodules}" Command="{Binding UpdateSubmodules}"
IsVisible="{Binding Submodules, Converter={x:Static c:ListConverters.IsNotNullOrEmpty}}" IsVisible="{Binding Submodules, Converter={x:Static c:ListConverters.IsNotNullOrEmpty}}"
ToolTip.Tip="{DynamicResource Text.Repository.Submodules.Update}"> ToolTip.Tip="{DynamicResource Text.Repository.Submodules.Update}">
<Path x:Name="iconSubmoduleUpdate" Width="12" Height="12" Data="{StaticResource Icons.Loading}"/> <Path Width="12" Height="12" Data="{StaticResource Icons.Loading}"/>
</Button> </Button>
<Button Grid.Column="3" <Button Grid.Column="3"
Classes="icon_button" Classes="icon_button"
@ -531,6 +531,96 @@
</DataGridTemplateColumn> </DataGridTemplateColumn>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
<!-- Worktrees -->
<ToggleButton Grid.Row="10" Classes="group_expander" IsChecked="{Binding IsWorktreeGroupExpanded, Mode=TwoWay}">
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock Grid.Column="0" Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.Worktrees}"/>
<TextBlock Grid.Column="1" Text="{Binding Worktrees, Converter={x:Static c:ListConverters.ToCount}}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/>
<Button Grid.Column="2"
Classes="icon_button"
Width="14"
Margin="8,0"
Command="{Binding PruneWorktrees}"
IsVisible="{Binding Worktrees, Converter={x:Static c:ListConverters.IsNotNullOrEmpty}}"
ToolTip.Tip="{DynamicResource Text.Repository.Worktrees.Prune}">
<Path x:Name="icon" 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 AddWorktree}"
ToolTip.Tip="{DynamicResource Text.Repository.Worktrees.Add}">
<Path Width="12" Height="12" Data="{StaticResource Icons.Worktree.Add}"/>
</Button>
</Grid>
</ToggleButton>
<DataGrid Grid.Row="11"
MaxHeight="200"
Margin="8,0,4,0"
Background="Transparent"
ItemsSource="{Binding Worktrees}"
SelectionMode="Single"
CanUserReorderColumns="False"
CanUserResizeColumns="False"
CanUserSortColumns="False"
IsReadOnly="True"
HeadersVisibility="None"
Focusable="False"
RowHeight="26"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"
ContextRequested="OnWorktreeContextRequested"
DoubleTapped="OnDoubleTappedWorktree"
IsVisible="{Binding IsWorktreeGroupExpanded, Mode=OneWay}">
<DataGrid.Styles>
<Style Selector="DataGridRow">
<Setter Property="CornerRadius" Value="4" />
</Style>
<Style Selector="DataGridRow /template/ Border#RowBorder">
<Setter Property="ClipToBounds" Value="True" />
</Style>
<Style Selector="DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource Brush.AccentHovered}" />
</Style>
<Style Selector="DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}" />
</Style>
</DataGrid.Styles>
<DataGrid.Columns>
<DataGridTemplateColumn Header="ICON">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Path Width="10" Height="10" Margin="8,0,0,0" Data="{StaticResource Icons.Worktree}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="*" Header="FullPath">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Classes="monospace" Margin="8,0,0,0" TextTrimming="CharacterEllipsis">
<Run Text="{Binding FullPath}"/>
<Run Text="{Binding Name}" Foreground="{DynamicResource Brush.FG2}"/>
</TextBlock>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="FullPath">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Path Width="10" Height="10" Margin="4,0,8,0" Data="{StaticResource Icons.Lock}" Fill="{DynamicResource Brush.FG2}" IsVisible="{Binding IsLocked}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid> </Grid>
<!-- Commit Search Panel --> <!-- Commit Search Panel -->

View file

@ -315,6 +315,40 @@ namespace SourceGit.Views
e.Handled = true; e.Handled = true;
} }
private void OnDoubleTappedSubmodule(object sender, TappedEventArgs e)
{
if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo)
{
var submodule = datagrid.SelectedItem as string;
(DataContext as ViewModels.Repository).OpenSubmodule(submodule);
}
e.Handled = true;
}
private void OnWorktreeContextRequested(object sender, ContextRequestedEventArgs e)
{
if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo)
{
var worktree = datagrid.SelectedItem as Models.Worktree;
var menu = repo.CreateContextMenuForWorktree(worktree);
datagrid.OpenContextMenu(menu);
}
e.Handled = true;
}
private void OnDoubleTappedWorktree(object sender, TappedEventArgs e)
{
if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo)
{
var worktree = datagrid.SelectedItem as Models.Worktree;
(DataContext as ViewModels.Repository).OpenWorktree(worktree);
}
e.Handled = true;
}
private void CollectBranchesFromNode(List<Models.Branch> outs, ViewModels.BranchTreeNode node) private void CollectBranchesFromNode(List<Models.Branch> outs, ViewModels.BranchTreeNode node)
{ {
if (node == null || node.IsRemote) if (node == null || node.IsRemote)
@ -332,16 +366,5 @@ namespace SourceGit.Views
outs.Add(b); outs.Add(b);
} }
} }
private void OnDoubleTappedSubmodule(object sender, TappedEventArgs e)
{
if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo)
{
var submodule = datagrid.SelectedItem as string;
(DataContext as ViewModels.Repository).OpenSubmodule(submodule);
}
e.Handled = true;
}
} }
} }