Skip to content

Commit fda2663

Browse files
committed
Stuff :D
1 parent f908a23 commit fda2663

21 files changed

+924
-27
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/but-api/src/commands/worktree.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ use crate::error::Error;
1818
pub fn worktree_new(
1919
project_id: ProjectId,
2020
reference: gix::refs::FullName,
21+
name: Option<String>,
2122
) -> Result<NewWorktreeOutcome, Error> {
2223
let project = gitbutler_project::get(project_id)?;
2324
let guard = project.exclusive_worktree_access();
2425
let mut ctx = CommandContext::open(&project, AppSettings::load_from_default_path_creating()?)?;
2526

26-
but_worktrees::new::worktree_new(&mut ctx, guard.read_permission(), reference.as_ref())
27+
but_worktrees::new::worktree_new(&mut ctx, guard.read_permission(), reference.as_ref(), name)
2728
.map_err(Into::into)
2829
}
2930

crates/but-worktrees/src/new.rs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,54 @@ use crate::{Worktree, WorktreeId, WorktreeMeta, db::save_worktree_meta, git::git
1212
/// This gets used as a public API in the CLI so be careful when modifying.
1313
pub struct NewWorktreeOutcome {
1414
pub created: Worktree,
15+
/// The git branch name created for this worktree (e.g., "gitbutler/worktree/name-a")
16+
pub branch_name: String,
17+
/// The commit message (first line) of the base commit
18+
pub base_commit_message: Option<String>,
19+
}
20+
21+
/// Generates a unique branch name by appending letters (-a, -b, -c, etc.) if needed.
22+
///
23+
/// Returns the deduplicated name without the "gitbutler/worktree/" prefix.
24+
fn deduplicate_branch_name(repo: &gix::Repository, base_name: &str) -> Result<String> {
25+
let mut name = base_name.to_string();
26+
27+
// Check if the base name already exists
28+
let full_ref = format!("refs/heads/gitbutler/worktree/{}", name);
29+
if gix::refs::FullName::try_from(full_ref.clone())
30+
.ok()
31+
.and_then(|r| repo.find_reference(&r).ok())
32+
.is_none()
33+
{
34+
return Ok(name);
35+
}
36+
37+
// Try appending letters a-z
38+
for c in 'a'..='z' {
39+
name = format!("{}-{}", base_name, c);
40+
let full_ref = format!("refs/heads/gitbutler/worktree/{}", name);
41+
if gix::refs::FullName::try_from(full_ref.clone())
42+
.ok()
43+
.and_then(|r| repo.find_reference(&r).ok())
44+
.is_none()
45+
{
46+
return Ok(name);
47+
}
48+
}
49+
50+
// If we exhausted all letters, fall back to UUID
51+
Ok(WorktreeId::new().as_str().to_string())
1552
}
1653

1754
/// Creates a new worktree off of a branches given name.
55+
///
56+
/// # Parameters
57+
/// - `name`: Optional custom name for the worktree branch. If None, generates a UUID.
1858
pub fn worktree_new(
1959
ctx: &mut CommandContext,
2060
perm: &WorktreeReadPermission,
2161
refname: &gix::refs::FullNameRef,
62+
name: Option<String>,
2263
) -> Result<NewWorktreeOutcome> {
2364
let repo = ctx.gix_repo_for_merging()?;
2465

@@ -30,12 +71,22 @@ pub fn worktree_new(
3071

3172
let to_checkout = repo.find_reference(refname)?.id();
3273

33-
// Generate a new worktree ID
34-
let id = WorktreeId::new();
74+
// Determine the branch name to use
75+
let base_name = if let Some(custom_name) = name {
76+
custom_name
77+
} else {
78+
refname.shorten().to_string()
79+
};
80+
81+
// Deduplicate the branch name
82+
let deduplicated_name = deduplicate_branch_name(&repo, &base_name)?;
83+
84+
// Generate a new worktree ID from the deduplicated name
85+
let id = WorktreeId::from_bstr(deduplicated_name.clone());
3586

3687
let path = worktree_path(ctx.project(), &id);
3788
let branch_name =
38-
gix::refs::PartialName::try_from(format!("gitbutler/worktree/{}", id.as_str()))?;
89+
gix::refs::PartialName::try_from(format!("gitbutler/worktree/{}", deduplicated_name))?;
3990

4091
git_worktree_add(
4192
&ctx.project().common_git_dir()?,
@@ -46,6 +97,13 @@ pub fn worktree_new(
4697

4798
let path = path.canonicalize()?;
4899

100+
// Get the commit message for the base commit
101+
let base_commit_message = repo
102+
.find_object(to_checkout.detach())
103+
.ok()
104+
.and_then(|obj| obj.try_into_commit().ok())
105+
.and_then(|commit| commit.message().ok().map(|msg| msg.title.to_string()));
106+
49107
let meta = WorktreeMeta {
50108
id: id.clone(),
51109
created_from_ref: Some(refname.to_owned()),
@@ -61,6 +119,8 @@ pub fn worktree_new(
61119
path,
62120
base: Some(to_checkout.detach()),
63121
},
122+
branch_name: branch_name.as_ref().to_string(),
123+
base_commit_message,
64124
})
65125
}
66126

crates/but-worktrees/tests/worktree/integrate.rs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ fn test_create_unrelated_change_and_reintroduce() -> anyhow::Result<()> {
1919

2020
let feature_a_name = gix::refs::FullName::try_from("refs/heads/feature-a")?;
2121
let feature_b_name = gix::refs::FullName::try_from("refs/heads/feature-b")?;
22-
let a = worktree_new(&mut ctx, guard.read_permission(), feature_a_name.as_ref())?;
22+
let a = worktree_new(
23+
&mut ctx,
24+
guard.read_permission(),
25+
feature_a_name.as_ref(),
26+
None,
27+
)?;
2328

2429
bash_at(
2530
&a.created.path,
@@ -99,7 +104,12 @@ fn test_causes_conflicts_above() -> anyhow::Result<()> {
99104

100105
let feature_a_name = gix::refs::FullName::try_from("refs/heads/feature-a")?;
101106
let feature_b_name = gix::refs::FullName::try_from("refs/heads/feature-b")?;
102-
let a = worktree_new(&mut ctx, guard.read_permission(), feature_a_name.as_ref())?;
107+
let a = worktree_new(
108+
&mut ctx,
109+
guard.read_permission(),
110+
feature_a_name.as_ref(),
111+
None,
112+
)?;
103113

104114
bash_at(
105115
&a.created.path,
@@ -182,7 +192,12 @@ fn test_causes_workdir_conflicts_simple() -> anyhow::Result<()> {
182192
let mut guard = ctx.project().exclusive_worktree_access();
183193

184194
let feature_b_name = gix::refs::FullName::try_from("refs/heads/feature-b")?;
185-
let b = worktree_new(&mut ctx, guard.read_permission(), feature_b_name.as_ref())?;
195+
let b = worktree_new(
196+
&mut ctx,
197+
guard.read_permission(),
198+
feature_b_name.as_ref(),
199+
None,
200+
)?;
186201

187202
bash_at(&path, r#"echo "qux" > foo.txt"#)?;
188203
bash_at(
@@ -239,7 +254,12 @@ fn test_causes_workdir_conflicts_complex() -> anyhow::Result<()> {
239254

240255
let feature_a_name = gix::refs::FullName::try_from("refs/heads/feature-a")?;
241256
let feature_b_name = gix::refs::FullName::try_from("refs/heads/feature-b")?;
242-
let a = worktree_new(&mut ctx, guard.read_permission(), feature_a_name.as_ref())?;
257+
let a = worktree_new(
258+
&mut ctx,
259+
guard.read_permission(),
260+
feature_a_name.as_ref(),
261+
None,
262+
)?;
243263

244264
bash_at(&path, r#"echo "qux" > foo.txt"#)?;
245265
bash_at(
@@ -310,7 +330,12 @@ fn test_causes_workspace_conflict() -> anyhow::Result<()> {
310330
let feature_a_name = gix::refs::FullName::try_from("refs/heads/feature-a")?;
311331
let feature_b_name = gix::refs::FullName::try_from("refs/heads/feature-b")?;
312332
let feature_c_name = gix::refs::FullName::try_from("refs/heads/feature-c")?;
313-
let c = worktree_new(&mut ctx, guard.read_permission(), feature_c_name.as_ref())?;
333+
let c = worktree_new(
334+
&mut ctx,
335+
guard.read_permission(),
336+
feature_c_name.as_ref(),
337+
None,
338+
)?;
314339

315340
bash_at(
316341
&c.created.path,

crates/but-worktrees/tests/worktree/main.rs

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ mod worktree_new {
7070
&mut test_ctx.ctx,
7171
guard.read_permission(),
7272
feature_a_name.as_ref(),
73+
None,
7374
)?;
7475

7576
assert_eq!(
@@ -118,6 +119,7 @@ mod worktree_new {
118119
&mut test_ctx.ctx,
119120
guard.read_permission(),
120121
feature_b_name.as_ref(),
122+
None,
121123
)?;
122124

123125
assert_eq!(
@@ -166,6 +168,7 @@ mod worktree_new {
166168
&mut test_ctx.ctx,
167169
guard.read_permission(),
168170
feature_c_name.as_ref(),
171+
None,
169172
)?;
170173

171174
assert_eq!(
@@ -205,11 +208,36 @@ mod worktree_list {
205208

206209
let feature_a_name = gix::refs::FullName::try_from("refs/heads/feature-a")?;
207210
let feature_c_name = gix::refs::FullName::try_from("refs/heads/feature-c")?;
208-
let a = worktree_new(&mut ctx, guard.read_permission(), feature_a_name.as_ref())?;
209-
let b = worktree_new(&mut ctx, guard.read_permission(), feature_a_name.as_ref())?;
210-
let c = worktree_new(&mut ctx, guard.read_permission(), feature_a_name.as_ref())?;
211-
let d = worktree_new(&mut ctx, guard.read_permission(), feature_a_name.as_ref())?;
212-
let e = worktree_new(&mut ctx, guard.read_permission(), feature_c_name.as_ref())?;
211+
let a = worktree_new(
212+
&mut ctx,
213+
guard.read_permission(),
214+
feature_a_name.as_ref(),
215+
None,
216+
)?;
217+
let b = worktree_new(
218+
&mut ctx,
219+
guard.read_permission(),
220+
feature_a_name.as_ref(),
221+
None,
222+
)?;
223+
let c = worktree_new(
224+
&mut ctx,
225+
guard.read_permission(),
226+
feature_a_name.as_ref(),
227+
None,
228+
)?;
229+
let d = worktree_new(
230+
&mut ctx,
231+
guard.read_permission(),
232+
feature_a_name.as_ref(),
233+
None,
234+
)?;
235+
let e = worktree_new(
236+
&mut ctx,
237+
guard.read_permission(),
238+
feature_c_name.as_ref(),
239+
None,
240+
)?;
213241

214242
let all = &[&a, &b, &c, &d, &e];
215243

@@ -243,7 +271,12 @@ mod worktree_destroy {
243271
let mut guard = ctx.project().exclusive_worktree_access();
244272

245273
let feature_a_name = gix::refs::FullName::try_from("refs/heads/feature-a")?;
246-
let outcome = worktree_new(&mut ctx, guard.read_permission(), feature_a_name.as_ref())?;
274+
let outcome = worktree_new(
275+
&mut ctx,
276+
guard.read_permission(),
277+
feature_a_name.as_ref(),
278+
None,
279+
)?;
247280

248281
// Verify it was created
249282
let list_before = worktree_list(&mut ctx, guard.read_permission())?;
@@ -275,11 +308,36 @@ mod worktree_destroy {
275308
let feature_c_name = gix::refs::FullName::try_from("refs/heads/feature-c")?;
276309

277310
// Create 3 worktrees from feature-a and 2 from feature-c
278-
worktree_new(&mut ctx, guard.read_permission(), feature_a_name.as_ref())?;
279-
worktree_new(&mut ctx, guard.read_permission(), feature_a_name.as_ref())?;
280-
worktree_new(&mut ctx, guard.read_permission(), feature_a_name.as_ref())?;
281-
worktree_new(&mut ctx, guard.read_permission(), feature_c_name.as_ref())?;
282-
worktree_new(&mut ctx, guard.read_permission(), feature_c_name.as_ref())?;
311+
worktree_new(
312+
&mut ctx,
313+
guard.read_permission(),
314+
feature_a_name.as_ref(),
315+
None,
316+
)?;
317+
worktree_new(
318+
&mut ctx,
319+
guard.read_permission(),
320+
feature_a_name.as_ref(),
321+
None,
322+
)?;
323+
worktree_new(
324+
&mut ctx,
325+
guard.read_permission(),
326+
feature_a_name.as_ref(),
327+
None,
328+
)?;
329+
worktree_new(
330+
&mut ctx,
331+
guard.read_permission(),
332+
feature_c_name.as_ref(),
333+
None,
334+
)?;
335+
worktree_new(
336+
&mut ctx,
337+
guard.read_permission(),
338+
feature_c_name.as_ref(),
339+
None,
340+
)?;
283341

284342
// Verify all 5 were created
285343
let list_before = worktree_list(&mut ctx, guard.read_permission())?;
@@ -318,7 +376,12 @@ mod worktree_destroy {
318376
let feature_b_name = gix::refs::FullName::try_from("refs/heads/feature-b")?;
319377

320378
// Create worktrees from feature-a
321-
worktree_new(&mut ctx, guard.read_permission(), feature_a_name.as_ref())?;
379+
worktree_new(
380+
&mut ctx,
381+
guard.read_permission(),
382+
feature_a_name.as_ref(),
383+
None,
384+
)?;
322385

323386
// Try to destroy worktrees from feature-b (which don't exist)
324387
let destroy_outcome = worktree_destroy_by_reference(

0 commit comments

Comments
 (0)