Skip to content

Commit 8a8aabe

Browse files
committed
feature: add worktree support (#205)
1 parent 43af8c4 commit 8a8aabe

23 files changed

+959
-21
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Opensource Git GUI client.
2121
* Tags
2222
* Stashes
2323
* Submodules
24+
* Worktrees
2425
* Archive
2526
* Diff
2627
* Save as patch/apply

src/Commands/Worktree.cs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text.RegularExpressions;
4+
5+
namespace SourceGit.Commands
6+
{
7+
public partial class Worktree : Command
8+
{
9+
[GeneratedRegex(@"^(\w)\s(\d+)$")]
10+
private static partial Regex REG_AHEAD_BEHIND();
11+
12+
public Worktree(string repo)
13+
{
14+
WorkingDirectory = repo;
15+
Context = repo;
16+
}
17+
18+
public List<Models.Worktree> List()
19+
{
20+
Args = "worktree list --porcelain";
21+
22+
var rs = ReadToEnd();
23+
var worktrees = new List<Models.Worktree>();
24+
var last = null as Models.Worktree;
25+
if (rs.IsSuccess)
26+
{
27+
var lines = rs.StdOut.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
28+
foreach (var line in lines)
29+
{
30+
if (line.StartsWith("worktree ", StringComparison.Ordinal))
31+
{
32+
last = new Models.Worktree() { FullPath = line.Substring(9).Trim() };
33+
worktrees.Add(last);
34+
}
35+
else if (line.StartsWith("bare", StringComparison.Ordinal))
36+
{
37+
last.IsBare = true;
38+
}
39+
else if (line.StartsWith("HEAD ", StringComparison.Ordinal))
40+
{
41+
last.Head = line.Substring(5).Trim();
42+
}
43+
else if (line.StartsWith("branch ", StringComparison.Ordinal))
44+
{
45+
last.Branch = line.Substring(7).Trim();
46+
}
47+
else if (line.StartsWith("detached", StringComparison.Ordinal))
48+
{
49+
last.IsDetached = true;
50+
}
51+
else if (line.StartsWith("locked", StringComparison.Ordinal))
52+
{
53+
last.IsLocked = true;
54+
}
55+
else if (line.StartsWith("prunable", StringComparison.Ordinal))
56+
{
57+
last.IsPrunable = true;
58+
}
59+
}
60+
}
61+
62+
return worktrees;
63+
}
64+
65+
public bool Add(string fullpath, string name, string tracking, Action<string> outputHandler)
66+
{
67+
Args = "worktree add ";
68+
69+
if (!string.IsNullOrEmpty(tracking))
70+
Args += "--track ";
71+
72+
if (!string.IsNullOrEmpty(name))
73+
Args += $"-b {name} ";
74+
75+
Args += $"\"{fullpath}\" ";
76+
77+
if (!string.IsNullOrEmpty(tracking))
78+
Args += tracking;
79+
80+
_outputHandler = outputHandler;
81+
return Exec();
82+
}
83+
84+
public bool Prune(Action<string> outputHandler)
85+
{
86+
Args = "worktree prune -v";
87+
_outputHandler = outputHandler;
88+
return Exec();
89+
}
90+
91+
public bool Lock(string fullpath, string reason)
92+
{
93+
if (string.IsNullOrEmpty(reason))
94+
Args = $"worktree lock \"{fullpath}\"";
95+
else
96+
Args = $"worktree lock --reason \"{reason}\" \"{fullpath}\"";
97+
return Exec();
98+
}
99+
100+
public bool Unlock(string fullpath)
101+
{
102+
Args = $"worktree unlock \"{fullpath}\"";
103+
return Exec();
104+
}
105+
106+
public bool Remove(string fullpath, bool force, Action<string> outputHandler)
107+
{
108+
if (force)
109+
Args = $"worktree remove -f \"{fullpath}\"";
110+
else
111+
Args = $"worktree remove \"{fullpath}\"";
112+
113+
_outputHandler = outputHandler;
114+
return Exec();
115+
}
116+
117+
protected override void OnReadline(string line)
118+
{
119+
_outputHandler?.Invoke(line);
120+
}
121+
122+
private Action<string> _outputHandler = null;
123+
}
124+
}

src/Models/Watcher.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public interface IRepository
1111
string GitDir { get; set; }
1212

1313
void RefreshBranches();
14+
void RefreshWorktrees();
1415
void RefreshTags();
1516
void RefreshCommits();
1617
void RefreshSubmodules();
@@ -132,6 +133,7 @@ private void Tick(object sender)
132133
}
133134

134135
Task.Run(_repo.RefreshWorkingCopyChanges);
136+
Task.Run(_repo.RefreshWorktrees);
135137
}
136138

137139
if (_updateWC > 0 && now > _updateWC)

src/Models/Worktree.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using CommunityToolkit.Mvvm.ComponentModel;
2+
3+
namespace SourceGit.Models
4+
{
5+
public class Worktree : ObservableObject
6+
{
7+
public string Branch { get; set; } = string.Empty;
8+
public string FullPath { get; set; } = string.Empty;
9+
public string Head { get; set; } = string.Empty;
10+
public bool IsBare { get; set; } = false;
11+
public bool IsDetached { get; set; } = false;
12+
public bool IsPrunable { get; set; } = false;
13+
14+
public bool IsLocked
15+
{
16+
get => _isLocked;
17+
set => SetProperty(ref _isLocked, value);
18+
}
19+
20+
public string Name
21+
{
22+
get
23+
{
24+
if (IsDetached)
25+
return $"(deteched HEAD at {Head.Substring(10)})";
26+
27+
if (Branch.StartsWith("refs/heads/", System.StringComparison.Ordinal))
28+
return $"({Branch.Substring(11)})";
29+
30+
if (Branch.StartsWith("refs/remotes/", System.StringComparison.Ordinal))
31+
return $"({Branch.Substring(13)})";
32+
33+
return $"({Branch})";
34+
}
35+
}
36+
37+
private bool _isLocked = false;
38+
}
39+
}

src/Resources/Icons.axaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,6 @@
104104
<StreamGeometry x:Key="Icons.Window.Maximize">M153 154h768v768h-768v-768zm64 64v640h640v-640h-640z</StreamGeometry>
105105
<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>
106106
<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>
107+
<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>
108+
<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>
107109
</ResourceDictionary>

src/Resources/Locales/en_US.axaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
<x:String x:Key="Text.About.Fonts" xml:space="preserve">• Monospace fonts come from </x:String>
88
<x:String x:Key="Text.About.SourceCode" xml:space="preserve">• Source code can be found at </x:String>
99
<x:String x:Key="Text.About.SubTitle" xml:space="preserve">Opensource &amp; Free Git GUI Client</x:String>
10+
<x:String x:Key="Text.AddWorktree" xml:space="preserve">Add Worktree</x:String>
11+
<x:String x:Key="Text.AddWorktree.Location" xml:space="preserve">Location:</x:String>
12+
<x:String x:Key="Text.AddWorktree.Name" xml:space="preserve">Branch Name:</x:String>
13+
<x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">Optional. Default is the destination folder name.</x:String>
14+
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">Track Branch:</x:String>
15+
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">Tracking remote branch</x:String>
1016
<x:String x:Key="Text.Apply" xml:space="preserve">Patch</x:String>
1117
<x:String x:Key="Text.Apply.Error" xml:space="preserve">Error</x:String>
1218
<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 @@
305311
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">ERROR</x:String>
306312
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">NOTICE</x:String>
307313
<x:String x:Key="Text.Launcher.Menu" xml:space="preserve">Open Main Menu</x:String>
314+
<x:String x:Key="Text.LockWorktree" xml:space="preserve">Lock Worktree</x:String>
315+
<x:String x:Key="Text.LockWorktree.Reason" xml:space="preserve">Reason:</x:String>
316+
<x:String x:Key="Text.LockWorktree.Reason.Placeholder" xml:space="preserve">Optional, specify a reason for the lock.</x:String>
317+
<x:String x:Key="Text.LockWorktree.Target" xml:space="preserve">Target:</x:String>
308318
<x:String x:Key="Text.Merge" xml:space="preserve">Merge Branch</x:String>
309319
<x:String x:Key="Text.Merge.Into" xml:space="preserve">Into:</x:String>
310320
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">Merge Option:</x:String>
@@ -367,6 +377,8 @@
367377
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">Tool</x:String>
368378
<x:String x:Key="Text.PruneRemote" xml:space="preserve">Prune Remote</x:String>
369379
<x:String x:Key="Text.PruneRemote.Target" xml:space="preserve">Target:</x:String>
380+
<x:String x:Key="Text.PruneWorktrees" xml:space="preserve">Prune Worktrees</x:String>
381+
<x:String x:Key="Text.PruneWorktrees.Tip" xml:space="preserve">Prune worktree information in `$GIT_DIR/worktrees`</x:String>
370382
<x:String x:Key="Text.Pull" xml:space="preserve">Pull</x:String>
371383
<x:String x:Key="Text.Pull.Branch" xml:space="preserve">Branch:</x:String>
372384
<x:String x:Key="Text.Pull.Into" xml:space="preserve">Into:</x:String>
@@ -408,6 +420,9 @@
408420
<x:String x:Key="Text.RemoteCM.OpenInBrowser" xml:space="preserve">Open In Browser</x:String>
409421
<x:String x:Key="Text.RemoteCM.Prune" xml:space="preserve">Prune</x:String>
410422
<x:String x:Key="Text.RemoteCM.Prune.Target" xml:space="preserve">Target:</x:String>
423+
<x:String x:Key="Text.RemoveWorktree" xml:space="preserve">Confirm to Remove Worktree</x:String>
424+
<x:String x:Key="Text.RemoveWorktree.Force" xml:space="preserve">Enable `--force` Option</x:String>
425+
<x:String x:Key="Text.RemoveWorktree.Target" xml:space="preserve">Target:</x:String>
411426
<x:String x:Key="Text.RenameBranch" xml:space="preserve">Rename Branch</x:String>
412427
<x:String x:Key="Text.RenameBranch.Name" xml:space="preserve">New Name:</x:String>
413428
<x:String x:Key="Text.RenameBranch.Name.Placeholder" xml:space="preserve">Unique name for this branch</x:String>
@@ -441,6 +456,9 @@
441456
<x:String x:Key="Text.Repository.Tags" xml:space="preserve">TAGS</x:String>
442457
<x:String x:Key="Text.Repository.Tags.Add" xml:space="preserve">NEW TAG</x:String>
443458
<x:String x:Key="Text.Repository.Terminal" xml:space="preserve">Open In Terminal</x:String>
459+
<x:String x:Key="Text.Repository.Worktrees" xml:space="preserve">WORKTREES</x:String>
460+
<x:String x:Key="Text.Repository.Worktrees.Add" xml:space="preserve">ADD WORKTREE</x:String>
461+
<x:String x:Key="Text.Repository.Worktrees.Prune" xml:space="preserve">PRUNE</x:String>
444462
<x:String x:Key="Text.RepositoryURL" xml:space="preserve">Git Repository URL</x:String>
445463
<x:String x:Key="Text.Reset" xml:space="preserve">Reset Current Branch To Revision</x:String>
446464
<x:String x:Key="Text.Reset.Mode" xml:space="preserve">Reset Mode:</x:String>
@@ -541,4 +559,8 @@
541559
<x:String x:Key="Text.WorkingCopy.Unstaged.ViewAssumeUnchaged" xml:space="preserve">VIEW ASSUME UNCHANGED</x:String>
542560
<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>
543561
<x:String x:Key="Text.Worktree" xml:space="preserve">WORKTREE</x:String>
562+
<x:String x:Key="Text.Worktree.CopyPath" xml:space="preserve">Copy Path</x:String>
563+
<x:String x:Key="Text.Worktree.Lock" xml:space="preserve">Lock</x:String>
564+
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">Remove</x:String>
565+
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">Unlock</x:String>
544566
</ResourceDictionary>

src/Resources/Locales/zh_CN.axaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
<x:String x:Key="Text.About.Fonts" xml:space="preserve">• 等宽字体来自于 </x:String>
1111
<x:String x:Key="Text.About.SourceCode" xml:space="preserve">• 项目源代码地址 </x:String>
1212
<x:String x:Key="Text.About.SubTitle" xml:space="preserve">开源免费的Git客户端</x:String>
13+
<x:String x:Key="Text.AddWorktree" xml:space="preserve">新增工作树</x:String>
14+
<x:String x:Key="Text.AddWorktree.Location" xml:space="preserve">工作树路径 :</x:String>
15+
<x:String x:Key="Text.AddWorktree.Name" xml:space="preserve">自定义分支名 :</x:String>
16+
<x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">选填。默认使用目标文件夹名称。</x:String>
17+
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">跟踪分支</x:String>
18+
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">设置上游跟踪分支</x:String>
1319
<x:String x:Key="Text.Apply" xml:space="preserve">应用补丁(apply)</x:String>
1420
<x:String x:Key="Text.Apply.Error" xml:space="preserve">错误</x:String>
1521
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">输出错误,并终止应用补丁</x:String>
@@ -308,6 +314,10 @@
308314
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">出错了</x:String>
309315
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">系统提示</x:String>
310316
<x:String x:Key="Text.Launcher.Menu" xml:space="preserve">主菜单</x:String>
317+
<x:String x:Key="Text.LockWorktree" xml:space="preserve">锁定工作树</x:String>
318+
<x:String x:Key="Text.LockWorktree.Reason" xml:space="preserve">原因 :</x:String>
319+
<x:String x:Key="Text.LockWorktree.Reason.Placeholder" xml:space="preserve">选填,为此锁定操作描述原因。</x:String>
320+
<x:String x:Key="Text.LockWorktree.Target" xml:space="preserve">目标工作树 :</x:String>
311321
<x:String x:Key="Text.Merge" xml:space="preserve">合并分支</x:String>
312322
<x:String x:Key="Text.Merge.Into" xml:space="preserve">目标分支 :</x:String>
313323
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">合并方式 :</x:String>
@@ -370,6 +380,8 @@
370380
<x:String x:Key="Text.Preference.DiffMerge.Type" xml:space="preserve">工具</x:String>
371381
<x:String x:Key="Text.PruneRemote" xml:space="preserve">清理远程已删除分支</x:String>
372382
<x:String x:Key="Text.PruneRemote.Target" xml:space="preserve">目标 :</x:String>
383+
<x:String x:Key="Text.PruneWorktrees" xml:space="preserve">清理工作树</x:String>
384+
<x:String x:Key="Text.PruneWorktrees.Tip" xml:space="preserve">清理在`$GIT_DIR/worktrees`中的无效工作树信息</x:String>
373385
<x:String x:Key="Text.Pull" xml:space="preserve">拉回(pull)</x:String>
374386
<x:String x:Key="Text.Pull.Branch" xml:space="preserve">拉取分支 :</x:String>
375387
<x:String x:Key="Text.Pull.Into" xml:space="preserve">本地分支 :</x:String>
@@ -410,6 +422,9 @@
410422
<x:String x:Key="Text.RemoteCM.Fetch" xml:space="preserve">拉取(fetch)更新</x:String>
411423
<x:String x:Key="Text.RemoteCM.OpenInBrowser" xml:space="preserve">在浏览器中打开</x:String>
412424
<x:String x:Key="Text.RemoteCM.Prune" xml:space="preserve">清理远程已删除分支</x:String>
425+
<x:String x:Key="Text.RemoveWorktree" xml:space="preserve">移除工作树操作确认</x:String>
426+
<x:String x:Key="Text.RemoveWorktree.Force" xml:space="preserve">启用`--force`选项</x:String>
427+
<x:String x:Key="Text.RemoveWorktree.Target" xml:space="preserve">目标工作树 :</x:String>
413428
<x:String x:Key="Text.RenameBranch" xml:space="preserve">分支重命名</x:String>
414429
<x:String x:Key="Text.RenameBranch.Name" xml:space="preserve">新的名称 :</x:String>
415430
<x:String x:Key="Text.RenameBranch.Name.Placeholder" xml:space="preserve">新的分支名不能与现有分支名相同</x:String>
@@ -443,6 +458,9 @@
443458
<x:String x:Key="Text.Repository.Tags" xml:space="preserve">标签列表</x:String>
444459
<x:String x:Key="Text.Repository.Tags.Add" xml:space="preserve">新建标签</x:String>
445460
<x:String x:Key="Text.Repository.Terminal" xml:space="preserve">在终端中打开</x:String>
461+
<x:String x:Key="Text.Repository.Worktrees" xml:space="preserve">工作树列表</x:String>
462+
<x:String x:Key="Text.Repository.Worktrees.Add" xml:space="preserve">新增工作树</x:String>
463+
<x:String x:Key="Text.Repository.Worktrees.Prune" xml:space="preserve">清理</x:String>
446464
<x:String x:Key="Text.RepositoryURL" xml:space="preserve">远程仓库地址</x:String>
447465
<x:String x:Key="Text.Reset" xml:space="preserve">重置(reset)当前分支到指定版本</x:String>
448466
<x:String x:Key="Text.Reset.Mode" xml:space="preserve">重置模式 :</x:String>
@@ -543,4 +561,8 @@
543561
<x:String x:Key="Text.WorkingCopy.Unstaged.ViewAssumeUnchaged" xml:space="preserve">查看忽略变更文件</x:String>
544562
<x:String x:Key="Text.WorkingCopy.ResolveTip" xml:space="preserve">请选中冲突文件,打开右键菜单,选择合适的解决方式</x:String>
545563
<x:String x:Key="Text.Worktree" xml:space="preserve">本地工作树</x:String>
564+
<x:String x:Key="Text.Worktree.CopyPath" xml:space="preserve">复制工作树路径</x:String>
565+
<x:String x:Key="Text.Worktree.Lock" xml:space="preserve">锁定工作树</x:String>
566+
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">移除工作树</x:String>
567+
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">解除工作树锁定</x:String>
546568
</ResourceDictionary>

0 commit comments

Comments
 (0)