Skip to content

Add worktree support#252

Merged
jeroen merged 7 commits intomainfrom
feature/worktrees
Jan 5, 2026
Merged

Add worktree support#252
jeroen merged 7 commits intomainfrom
feature/worktrees

Conversation

@DavisVaughan
Copy link
Copy Markdown
Member

@DavisVaughan DavisVaughan commented Dec 30, 2025

@jeroen can we fix the failing tests on main first? I think if you run CI on main right now, it will fail?


Closes #251

Being used in DavisVaughan/cross#9

libgit2 C API for worktrees:
https://libgit2.org/docs/reference/main/worktree/index.html

// Exposed on R side
git_worktree_list
git_worktree_validate // exposed as non-erroring git_worktree_is_valid
git_worktree_add
git_worktree_lock
git_worktree_unlock
git_worktree_is_locked
git_worktree_path
git_worktree_is_prunable
git_worktree_prune

// Not exposed
git_worktree_lookup // used on C side to load a worktree
git_worktree_open_from_repository // not used, doesn't make sense for us
git_worktree_free // used on C side
git_worktree_name // doesn't make sense, the `name` is our user provided input
git_worktree_prune_options_init // not used
git_worktree_add_options_init // not used

Also exposed on the R side are:

  • git_worktree_exists(), which returns TRUE or FALSE based on the result of git_worktree_lookup()
  • git_worktree_remove(), a git_worktree_prune() wrapper with more aggressive defaults, mimicking git worktree remove. More on this below.

All functions use a worktree's name as the way to uniquely identify it and interact with it, rather than handing out some kind of S3 object for a worktree (i.e. it follows the gert model of using simple R objects).

The main useful functions for working with worktrees are:

  • git_worktree_list()
  • git_worktree_add()
  • git_worktree_remove()

git_worktree_add()

The libgit2 version of this function is quite complicated and confusing because both libgit2 and command line git mix worktree creation and branch creation together into a single git worktree add utility. This ends up being extremely confusing, because git worktree add will sometimes create a new branch for you in addition to the worktree, but git worktree remove will never delete that branch.

I have written notes about this above the C signature of R_git_worktree_add(). I'd encourage you to read those notes then come back here:
https://github.com/r-lib/gert/pull/252/changes#diff-901344d7a8b2466c8b7d4f499b8dde3f78c2cb02eac92c193d86f5b156f39db5R116-R143

Basically, if we mimicked libgit2 exactly then our defaults would encourage writing code that creates temporary worktrees like this:

# This is pseudocode, not the actual impl:

# This both creates a worktree and creates a branch named `foo` that branches from HEAD
git_worktree_add(name = "foo", path = "path/for/foo")
withr::defer(git_branch_delete("foo")) # It's confusing that this is needed
withr::defer(git_worktree_remove("foo"))

do_thing_with_worktree()

I found that extremely confusing. If you forget the git_branch_delete() then you can't git_worktree_add("foo") a second time, because the "foo" branch already exists.

IMO it is much cleaner if we force branch to be provided - meaning that git_worktree_add()'s sole job is worktree creation. So instead I ended up with:

git_branch_create("foo")
withr::defer(git_branch_delete("foo"))

git_worktree_add(name = "worktree", path = "path/for/worktree", branch = "foo")
withr::defer(git_worktree_remove("worktree"))

do_thing_with_worktree()

This made wayyyyyyyy more sense to me.

git_worktree_remove()

Command line git has both git worktree prune and git worktree remove

  • When you are "done with" a worktree, you are ideally supposed to remove it. This removes the git metadata for it in the main working tree and throws away the whole folder that the worktree was in (the path arg from above). That's nice!

  • If you manually deleted the worktree folder but forgot to run remove, then git metadata for the worktree is left lying around in the main working tree. This is automatically cleaned up by git by running git worktree prune at regular intervals. So git_worktree_prune() is really more of a gentle "behind the scenes" way to clean up old worktrees. It isn't meant for interactively removing them.

libgit2 exposed these git worktree features in a weird way. Rather than exposing both git_worktree_prune() and git_worktree_remove(), the libgit2 API only has git_worktree_prune(), but you can set a bunch of options to make it work like git worktree remove. I've chosen to expose git_worktree_remove() on the R side as a wrapper around git_worktree_prune() that makes it actually useful for interactive invocations.

Comment thread src/init.c
{"R_git_worktree_lock", (DL_FUNC) &R_git_worktree_lock, 2},
{"R_git_worktree_unlock", (DL_FUNC) &R_git_worktree_unlock, 2},
{"R_git_worktree_add", (DL_FUNC) &R_git_worktree_add, 6},
{"R_git_worktree_is_prunable",(DL_FUNC) &R_git_worktree_is_prunable,4},
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

&R_git_worktree_is_prunable,4}, could probably use a space before the 4, but I didn't want to have to realign everything to do so. I'll let you do this if you want to.

Comment thread src/worktree.c
Comment on lines +59 to +61
// Throws away reason for why it is not valid. Could add
// `git_worktree_check_valid()` to propagate the reason up.
SEXP R_git_worktree_is_valid(SEXP ptr, SEXP name) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few functions like R_git_worktree_is_valid() currently throw away a bit of info to be more R-like.

For example, the git function git_worktree_validate() returns 0 or an error code. We just return TRUE or FALSE so we lose the error information here.

An R level git_worktree_check_valid() would retain that error info as part of an error message, but I don't think it is worth it right now.

@jeroen
Copy link
Copy Markdown
Member

jeroen commented Jan 5, 2026

Sorry about breaking up the CI :| Now fixed in the main branch and merged here.

@DavisVaughan
Copy link
Copy Markdown
Member Author

Debian 11 fails but not 10 or 12? Fun 😬

@jeroen
Copy link
Copy Markdown
Member

jeroen commented Jan 5, 2026

We can ignore it, not many people use Debian 10/11 anymore. Let's skip the test for old versions of libgit2; as long as it at least can be built on Debian 11 it is fine, for some old servers that pull this package in as a dependency.

skip_if_not(libgit2_config()$version > "1.1.0")

@DavisVaughan DavisVaughan marked this pull request as ready for review January 5, 2026 18:47
@DavisVaughan DavisVaughan requested a review from jeroen January 5, 2026 18:47
@jeroen
Copy link
Copy Markdown
Member

jeroen commented Jan 5, 2026

I think all other R functions have repo as the last parameter. The reasoning is that usually you are working from "." so this parameter is typically set to default.

Would it make sense to change the order of parameters in git_worktree_prune and git_worktree_remove and so on to move repo to the end?

@DavisVaughan
Copy link
Copy Markdown
Member Author

Done!

@jeroen jeroen merged commit f681605 into main Jan 5, 2026
29 checks passed
@jeroen jeroen deleted the feature/worktrees branch January 5, 2026 20:12
@jeroen
Copy link
Copy Markdown
Member

jeroen commented Jan 5, 2026

Thanks!

@jeroen
Copy link
Copy Markdown
Member

jeroen commented Jan 8, 2026

This has landed on CRAN now so you can depend on it (but feel free to do further tweaking).

@DavisVaughan
Copy link
Copy Markdown
Member Author

thanks!

@jeroen
Copy link
Copy Markdown
Member

jeroen commented Jan 8, 2026

FWI cran ASAN found a bug in the R_git_worktree_add() declaration (it has 7 parameters instead of 6).

@DavisVaughan
Copy link
Copy Markdown
Member Author

Darn sorry about that, would you be able to fix it for me? 😞

@jeroen
Copy link
Copy Markdown
Member

jeroen commented Jan 8, 2026

Yes I don't think it will lead to crashes, it's just a warning about declaration mismatch. So not urgent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

git_worktree_add() and git_worktree_remove() / git_worktree_delete()

2 participants