Skip to content

Commit f7041b9

Browse files
committed
refactor: worktree
- `Add Worktree` now only creates worktree based on HEAD - Supports to checkout a remote branch as worktree - Use different popups for `Add Worktree`, `Checkout <local_branch> as Worktree` and `Checkout <remote_branch> as Worktree`
1 parent c7db6b3 commit f7041b9

14 files changed

Lines changed: 493 additions & 215 deletions

‎src/Commands/Worktree.cs‎

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.IO;
34
using System.Text;
45
using System.Threading.Tasks;
56

@@ -61,20 +62,33 @@ public Worktree(string repo)
6162
return worktrees;
6263
}
6364

64-
public async Task<bool> AddAsync(string fullpath, string name, bool createNew, string tracking)
65+
public async Task<bool> AddAsync(string fullpath, string name)
66+
{
67+
if (string.IsNullOrEmpty(name))
68+
Args = $"worktree add {fullpath.Quoted()}";
69+
else
70+
Args = $"worktree add -b {name} {fullpath.Quoted()}";
71+
72+
return await ExecAsync().ConfigureAwait(false);
73+
}
74+
75+
public async Task<bool> AddWithLocalBranchAsync(string fullpath, string localBranch)
76+
{
77+
Args = $"worktree add -B {localBranch} {fullpath.Quoted()} {localBranch}";
78+
return await ExecAsync().ConfigureAwait(false);
79+
}
80+
81+
public async Task<bool> AddWithRemoteBranchAsync(string fullpath, string name, string remoteBranch, bool tracking)
6582
{
6683
var builder = new StringBuilder(1024);
6784
builder.Append("worktree add ");
68-
if (!string.IsNullOrEmpty(tracking))
69-
builder.Append("--track ");
70-
if (!string.IsNullOrEmpty(name))
71-
builder.Append(createNew ? "-b " : "-B ").Append(name).Append(' ');
72-
builder.Append(fullpath.Quoted()).Append(' ');
73-
74-
if (!string.IsNullOrEmpty(tracking))
75-
builder.Append(tracking);
76-
else if (!string.IsNullOrEmpty(name) && !createNew)
77-
builder.Append(name);
85+
86+
if (tracking)
87+
builder.Append("--track -b ").Append(string.IsNullOrEmpty(name) ? Path.GetFileName(fullpath) : name).Append(' ');
88+
else if (!string.IsNullOrEmpty(name))
89+
builder.Append("-b ").Append(name).Append(' ');
90+
91+
builder.Append(fullpath.Quoted()).Append(' ').Append(remoteBranch);
7892

7993
Args = builder.ToString();
8094
return await ExecAsync().ConfigureAwait(false);

‎src/Resources/Locales/en_US.axaml‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@
1212
<x:String x:Key="Text.AddWorktree.Location.Placeholder" xml:space="preserve">Path for this worktree. Relative path is supported.</x:String>
1313
<x:String x:Key="Text.AddWorktree.Name" xml:space="preserve">Branch Name:</x:String>
1414
<x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">Optional. Default is the destination folder name.</x:String>
15-
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">Track Branch:</x:String>
16-
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">Tracking remote branch</x:String>
17-
<x:String x:Key="Text.AddWorktree.WhatToCheckout" xml:space="preserve">What to Checkout:</x:String>
18-
<x:String x:Key="Text.AddWorktree.WhatToCheckout.CreateNew" xml:space="preserve">Create New Branch</x:String>
19-
<x:String x:Key="Text.AddWorktree.WhatToCheckout.Existing" xml:space="preserve">Existing Branch</x:String>
2015
<x:String x:Key="Text.AIAssistant" xml:space="preserve">AI Assistant</x:String>
2116
<x:String x:Key="Text.AIAssistant.Model" xml:space="preserve">MODEL</x:String>
2217
<x:String x:Key="Text.AIAssistant.Regen" xml:space="preserve">RE-GENERATE</x:String>
@@ -120,6 +115,14 @@
120115
<x:String x:Key="Text.Checkout.WarnUpdatingSubmodules" xml:space="preserve">The following submodules need to be updated:{0}Do you want to update them?</x:String>
121116
<x:String x:Key="Text.Checkout.WithFastForward" xml:space="preserve">Checkout &amp; Fast-Forward</x:String>
122117
<x:String x:Key="Text.Checkout.WithFastForward.Upstream" xml:space="preserve">Fast-Forward to:</x:String>
118+
<x:String x:Key="Text.CheckoutAsWorktree" xml:space="preserve">Checkout Branch as Worktree</x:String>
119+
<x:String x:Key="Text.CheckoutAsWorktree.LocalBranch" xml:space="preserve">Local Branch:</x:String>
120+
<x:String x:Key="Text.CheckoutAsWorktree.NewBranchName" xml:space="preserve">New Branch Name:</x:String>
121+
<x:String x:Key="Text.CheckoutAsWorktree.NewBranchName.Placeholder" xml:space="preserve">Optional. Default is the destination folder name.</x:String>
122+
<x:String x:Key="Text.CheckoutAsWorktree.RemoteBranch" xml:space="preserve">Remote Branch:</x:String>
123+
<x:String x:Key="Text.CheckoutAsWorktree.Tracking" xml:space="preserve">Tracking this remote branch.</x:String>
124+
<x:String x:Key="Text.CheckoutAsWorktree.WorktreePath" xml:space="preserve">Worktree Path:</x:String>
125+
<x:String x:Key="Text.CheckoutAsWorktree.WorktreePath.Placeholder" xml:space="preserve">Path for this worktree. Relative path is supported.</x:String>
123126
<x:String x:Key="Text.CheckoutBranchFromStash" xml:space="preserve">Checkout Branch From Stash</x:String>
124127
<x:String x:Key="Text.CheckoutBranchFromStash.Branch" xml:space="preserve">New Branch:</x:String>
125128
<x:String x:Key="Text.CheckoutBranchFromStash.Stash" xml:space="preserve">Stash:</x:String>

‎src/Resources/Locales/zh_CN.axaml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@
124124
<x:String x:Key="Text.Checkout.WarnUpdatingSubmodules" xml:space="preserve">以下子模块需要更新:{0}是否立即更新?</x:String>
125125
<x:String x:Key="Text.Checkout.WithFastForward" xml:space="preserve">检出分支并快进</x:String>
126126
<x:String x:Key="Text.Checkout.WithFastForward.Upstream" xml:space="preserve">上游分支 :</x:String>
127+
<x:String x:Key="Text.CheckoutAsWorktree" xml:space="preserve">检出分支为新的工作树</x:String>
128+
<x:String x:Key="Text.CheckoutAsWorktree.LocalBranch" xml:space="preserve">本地分支 :</x:String>
129+
<x:String x:Key="Text.CheckoutAsWorktree.NewBranchName" xml:space="preserve">新分支名称 :</x:String>
130+
<x:String x:Key="Text.CheckoutAsWorktree.NewBranchName.Placeholder" xml:space="preserve">选填。默认使用目标文件夹名称。</x:String>
131+
<x:String x:Key="Text.CheckoutAsWorktree.RemoteBranch" xml:space="preserve">远程分支 :</x:String>
132+
<x:String x:Key="Text.CheckoutAsWorktree.Tracking" xml:space="preserve">新分支跟踪此远程分支</x:String>
133+
<x:String x:Key="Text.CheckoutAsWorktree.WorktreePath" xml:space="preserve">工作树路径 :</x:String>
134+
<x:String x:Key="Text.CheckoutAsWorktree.WorktreePath.Placeholder" xml:space="preserve">必填。可填写相对路径。</x:String>
127135
<x:String x:Key="Text.CheckoutBranchFromStash" xml:space="preserve">从所选贮藏检出分支</x:String>
128136
<x:String x:Key="Text.CheckoutBranchFromStash.Branch" xml:space="preserve">新分支 :</x:String>
129137
<x:String x:Key="Text.CheckoutBranchFromStash.Stash" xml:space="preserve">所选贮藏 :</x:String>

‎src/ViewModels/AddWorktree.cs‎

Lines changed: 33 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.ComponentModel.DataAnnotations;
43
using System.IO;
54
using System.Threading.Tasks;
@@ -8,98 +7,35 @@ namespace SourceGit.ViewModels
87
{
98
public class AddWorktree : Popup
109
{
11-
[Required(ErrorMessage = "Worktree path is required!")]
1210
[CustomValidation(typeof(AddWorktree), nameof(ValidateWorktreePath))]
13-
public string Path
11+
public string WorktreePath
1412
{
15-
get => _path;
16-
set => SetProperty(ref _path, value, true);
13+
get => _worktreePath;
14+
set => SetProperty(ref _worktreePath, value, true);
1715
}
1816

19-
public bool CreateNewBranch
17+
[CustomValidation(typeof(AddWorktree), nameof(ValidateBranchName))]
18+
public string BranchName
2019
{
21-
get => _createNewBranch;
22-
set
23-
{
24-
if (SetProperty(ref _createNewBranch, value, true))
25-
{
26-
if (value)
27-
SelectedBranch = null;
28-
else
29-
SelectedBranch = LocalBranches.Count > 0 ? LocalBranches[0] : null;
30-
}
31-
}
32-
}
33-
34-
public List<Models.Branch> LocalBranches
35-
{
36-
get;
37-
private set;
38-
}
39-
40-
public Models.Branch SelectedBranch
41-
{
42-
get => _selectedBranch;
43-
set => SetProperty(ref _selectedBranch, value);
44-
}
45-
46-
public string NewBranchName
47-
{
48-
get => _newBranchName;
49-
set => SetProperty(ref _newBranchName, value);
50-
}
51-
52-
public bool SetTrackingBranch
53-
{
54-
get => _setTrackingBranch;
55-
set
56-
{
57-
if (SetProperty(ref _setTrackingBranch, value))
58-
AutoSelectTrackingBranch();
59-
}
60-
}
61-
62-
public List<Models.Branch> RemoteBranches
63-
{
64-
get;
65-
private set;
66-
}
67-
68-
public Models.Branch SelectedTrackingBranch
69-
{
70-
get => _selectedTrackingBranch;
71-
set => SetProperty(ref _selectedTrackingBranch, value);
20+
get => _branchName;
21+
set => SetProperty(ref _branchName, value, true);
7222
}
7323

7424
public AddWorktree(Repository repo)
7525
{
7626
_repo = repo;
77-
78-
LocalBranches = new List<Models.Branch>();
79-
RemoteBranches = new List<Models.Branch>();
80-
foreach (var branch in repo.Branches)
81-
{
82-
if (branch.IsLocal)
83-
{
84-
if (!branch.IsCurrent && !branch.HasWorktree)
85-
LocalBranches.Add(branch);
86-
}
87-
else
88-
{
89-
RemoteBranches.Add(branch);
90-
}
91-
}
27+
_worktreePath = Path.Combine(repo.GetRecommandedWorktreeDir(), "new-worktree");
9228
}
9329

9430
public static ValidationResult ValidateWorktreePath(string path, ValidationContext ctx)
9531
{
9632
if (ctx.ObjectInstance is not AddWorktree creator)
97-
return new ValidationResult("Missing runtime context to create branch!");
33+
return new ValidationResult("Missing runtime context to add worktree!");
9834

9935
if (string.IsNullOrEmpty(path))
10036
return new ValidationResult("Worktree path is required!");
10137

102-
var fullPath = System.IO.Path.IsPathRooted(path) ? path : System.IO.Path.Combine(creator._repo.FullPath, path);
38+
var fullPath = Path.IsPathRooted(path) ? path : Path.Combine(creator._repo.FullPath, path);
10339
var info = new DirectoryInfo(fullPath);
10440
if (info.Exists)
10541
{
@@ -115,70 +51,42 @@ public static ValidationResult ValidateWorktreePath(string path, ValidationConte
11551
return ValidationResult.Success;
11652
}
11753

118-
public override async Task<bool> Sure()
54+
public static ValidationResult ValidateBranchName(string name, ValidationContext ctx)
11955
{
120-
using var lockWatcher = _repo.LockWatcher();
121-
ProgressDescription = "Adding worktree ...";
122-
123-
var branchName = GetBranchName(false);
124-
var tracking = (_createNewBranch && _setTrackingBranch && _selectedTrackingBranch != null) ? _selectedTrackingBranch.FriendlyName : string.Empty;
125-
var log = _repo.CreateLog("Add Worktree");
126-
127-
Use(log);
128-
129-
var succ = await new Commands.Worktree(_repo.FullPath)
130-
.Use(log)
131-
.AddAsync(_path, branchName, _createNewBranch, tracking);
132-
133-
log.Complete();
134-
return succ;
135-
}
56+
if (ctx.ObjectInstance is not AddWorktree creator)
57+
return new ValidationResult("Missing runtime context to add worktree!");
13658

137-
private void AutoSelectTrackingBranch()
138-
{
139-
if (!_setTrackingBranch || RemoteBranches.Count == 0)
140-
return;
59+
var test = !string.IsNullOrEmpty(name) ? name : Path.GetFileName(creator._worktreePath.TrimEnd('/').TrimEnd('\\'));
60+
if (!Models.RefName.IsValidBranchName(test))
61+
return new ValidationResult($"{test} is not a valid branch name!");
14162

142-
var name = GetBranchName(true);
143-
if (!string.IsNullOrEmpty(name))
63+
foreach (var b in creator._repo.Branches)
14464
{
145-
var remoteBranch = RemoteBranches.Find(b => b.Name.Equals(name, StringComparison.Ordinal));
146-
remoteBranch ??= RemoteBranches.Find(b => b.Name.EndsWith("/" + name, StringComparison.Ordinal));
147-
if (remoteBranch != null)
148-
{
149-
SelectedTrackingBranch = remoteBranch;
150-
return;
151-
}
65+
if (b.IsLocal && b.Name.Equals(test, StringComparison.Ordinal))
66+
return new ValidationResult($"A branch with name '{test}' already exists!");
15267
}
15368

154-
SelectedTrackingBranch = RemoteBranches[0];
69+
return ValidationResult.Success;
15570
}
15671

157-
private string GetBranchName(bool fallback)
72+
public override async Task<bool> Sure()
15873
{
159-
do
160-
{
161-
if (!_createNewBranch)
162-
{
163-
if (_selectedBranch != null)
164-
return _selectedBranch.Name;
74+
using var lockWatcher = _repo.LockWatcher();
75+
ProgressDescription = $"Adding a new worktree ...";
16576

166-
break;
167-
}
77+
var log = _repo.CreateLog("Add Worktree");
78+
Use(log);
16879

169-
if (!string.IsNullOrEmpty(_newBranchName))
170-
return _newBranchName;
171-
} while (false);
80+
var succ = await new Commands.Worktree(_repo.FullPath)
81+
.Use(log)
82+
.AddAsync(_worktreePath, _branchName);
17283

173-
return fallback ? System.IO.Path.GetFileName(_path.TrimEnd('/', '\\')) : string.Empty;
84+
log.Complete();
85+
return succ;
17486
}
17587

176-
private Repository _repo = null;
177-
private string _path = string.Empty;
178-
private bool _createNewBranch = true;
179-
private Models.Branch _selectedBranch = null;
180-
private string _newBranchName = string.Empty;
181-
private bool _setTrackingBranch = false;
182-
private Models.Branch _selectedTrackingBranch = null;
88+
private readonly Repository _repo = null;
89+
private string _worktreePath = string.Empty;
90+
private string _branchName = string.Empty;
18391
}
18492
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
5+
namespace SourceGit.ViewModels
6+
{
7+
public class CheckoutAsWorktree : Popup
8+
{
9+
public Models.Branch Branch
10+
{
11+
get => _branch;
12+
}
13+
14+
[CustomValidation(typeof(CheckoutAsWorktree), nameof(ValidateWorktreePath))]
15+
public string WorktreePath
16+
{
17+
get => _worktreePath;
18+
set => SetProperty(ref _worktreePath, value, true);
19+
}
20+
21+
public CheckoutAsWorktree(Repository repo, Models.Branch branch)
22+
{
23+
_repo = repo;
24+
_branch = branch;
25+
_worktreePath = Path.Combine(repo.GetRecommandedWorktreeDir(), branch.Name.Replace('/', '-').Replace('\\', '-'));
26+
}
27+
28+
public static ValidationResult ValidateWorktreePath(string path, ValidationContext ctx)
29+
{
30+
if (ctx.ObjectInstance is not CheckoutAsWorktree creator)
31+
return new ValidationResult("Missing runtime context to checkout branch as worktree!");
32+
33+
if (string.IsNullOrEmpty(path))
34+
return new ValidationResult("Worktree path is required!");
35+
36+
var fullPath = Path.IsPathRooted(path) ? path : Path.Combine(creator._repo.FullPath, path);
37+
var info = new DirectoryInfo(fullPath);
38+
if (info.Exists)
39+
{
40+
var files = info.GetFiles();
41+
if (files.Length > 0)
42+
return new ValidationResult("Given path is not empty!!!");
43+
44+
var folders = info.GetDirectories();
45+
if (folders.Length > 0)
46+
return new ValidationResult("Given path is not empty!!!");
47+
}
48+
49+
return ValidationResult.Success;
50+
}
51+
52+
public override async Task<bool> Sure()
53+
{
54+
using var lockWatcher = _repo.LockWatcher();
55+
ProgressDescription = $"Checkout branch '{_branch.Name}' as worktree ...";
56+
57+
var log = _repo.CreateLog("Add Worktree");
58+
Use(log);
59+
60+
var succ = await new Commands.Worktree(_repo.FullPath)
61+
.Use(log)
62+
.AddWithLocalBranchAsync(_worktreePath, _branch.Name);
63+
64+
log.Complete();
65+
return succ;
66+
}
67+
68+
private readonly Repository _repo;
69+
private readonly Models.Branch _branch;
70+
private string _worktreePath;
71+
}
72+
}

0 commit comments

Comments
 (0)