Skip to content

Commit 728fe60

Browse files
committed
Add worktree support
1 parent 9a145b8 commit 728fe60

23 files changed

+1058
-16
lines changed

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@ VignetteBuilder:
3131
knitr
3232
Encoding: UTF-8
3333
Roxygen: list(markdown = TRUE)
34-
RoxygenNote: 7.3.2.9000
34+
RoxygenNote: 7.3.3
3535
SystemRequirements: libgit2 (>= 1.0): libgit2-devel (rpm) or libgit2-dev (deb)
3636
Language: en-US

NAMESPACE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@ export(git_tag_create)
7979
export(git_tag_delete)
8080
export(git_tag_list)
8181
export(git_tag_push)
82+
export(git_worktree_add)
83+
export(git_worktree_exists)
84+
export(git_worktree_is_locked)
85+
export(git_worktree_is_prunable)
86+
export(git_worktree_is_valid)
87+
export(git_worktree_list)
88+
export(git_worktree_lock)
89+
export(git_worktree_path)
90+
export(git_worktree_prune)
91+
export(git_worktree_remove)
92+
export(git_worktree_unlock)
8293
export(libgit2_config)
8394
export(user_is_configured)
8495
importFrom(askpass,askpass)
@@ -159,6 +170,16 @@ useDynLib(gert,R_git_submodule_update)
159170
useDynLib(gert,R_git_tag_create)
160171
useDynLib(gert,R_git_tag_delete)
161172
useDynLib(gert,R_git_tag_list)
173+
useDynLib(gert,R_git_worktree_add)
174+
useDynLib(gert,R_git_worktree_exists)
175+
useDynLib(gert,R_git_worktree_is_locked)
176+
useDynLib(gert,R_git_worktree_is_prunable)
177+
useDynLib(gert,R_git_worktree_is_valid)
178+
useDynLib(gert,R_git_worktree_list)
179+
useDynLib(gert,R_git_worktree_lock)
180+
useDynLib(gert,R_git_worktree_path)
181+
useDynLib(gert,R_git_worktree_prune)
182+
useDynLib(gert,R_git_worktree_unlock)
162183
useDynLib(gert,R_libgit2_config)
163184
useDynLib(gert,R_set_cert_locations)
164185
useDynLib(gert,R_static_libgit2)

R/worktree.R

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
#' Git Worktrees
2+
#'
3+
#' @description
4+
#' Worktrees represent an alternative location to checkout a branch into. Rather
5+
#' than checking out a branch in your main working tree (which changes the
6+
#' branch you are currently on and forces you to stash any existing work), you
7+
#' can instead check that branch out into a separate _linked worktree_ with its
8+
#' own working tree. Practically, a worktree is just a separate folder that a
9+
#' branch is checked out into, with some extra git metadata that links it back
10+
#' to the main working tree.
11+
#'
12+
#' `git_worktree_list()` returns a data frame of information about the worktrees
13+
#' linked to the main working tree.
14+
#'
15+
#' `git_worktree_exists()` lets you check whether or not a worktree by the name
16+
#' of `name` exists for this `repo`.
17+
#'
18+
#' `git_worktree_path()` returns the file path to the worktree.
19+
#'
20+
#' `git_worktree_add()` creates a new worktree called `name` in the folder
21+
#' pointed to by `path`, and checks `branch` out into it.
22+
#'
23+
#' `git_worktree_remove()` removes a worktree. It does so by deleting the folder
24+
#' provided as the `path` to `git_worktree_add()`, and then cleaning up some git
25+
#' metadata in the main working tree that linked the main working tree to the
26+
#' removed worktree. The `branch` checked out by the worktree is not deleted.
27+
#' Note that this is just a wrapper around `git_worktree_prune()` that sets some
28+
#' desirable defaults for aggressive removal.
29+
#'
30+
#' `git_worktree_prune()` is more cautious than `git_worktree_remove()`. It
31+
#' refuses to prune _valid_ or _locked_ worktrees by default, and also refuses
32+
#' the delete the working tree of the worktree by default (i.e. the folder at
33+
#' `path`). It is automatically run by git itself on periodic intervals to prune
34+
#' outdated worktrees. For interactive usage, you typically want
35+
#' `git_worktree_remove()` instead. `git_worktree_is_prunable()` lets you check
36+
#' if a worktree is prunable with the given options.
37+
#'
38+
#' `git_worktree_lock()`, `git_worktree_unlock()`, and
39+
#' `git_worktree_is_locked()` help you manage whether or not a worktree is
40+
#' _locked_. When a worktree is locked, it is not automatically cleaned up by
41+
#' `git_worktree_prune()` (and git itself) on periodic intervals, even when it
42+
#' looks _invalid_. This is typically only useful when your worktree is on a
43+
#' hard drive that isn't always connected (which can make it look _invalid_ when
44+
#' disconnected, typically making it a candidate for automatic pruning).
45+
#'
46+
#' `git_worktree_is_valid()` checks whether a worktree is valid or not. A
47+
#' _valid_ worktree requires both the git data structures inside the main
48+
#' working tree and this worktree to be present.
49+
#'
50+
#' @name git_worktree
51+
#' @family git
52+
#' @inheritParams git_open
53+
#'
54+
#' @param name The name of the worktree.
55+
#'
56+
#' @examples
57+
#' repo <- git_init(tempfile("gert-examples-repo"))
58+
#'
59+
#' writeLines("hello", file.path(repo, 'hello.txt'))
60+
#' git_add('hello.txt', repo = repo)
61+
#' git_commit("First commit", author = "jeroen <jeroen@blabla.nl>", repo = repo)
62+
#'
63+
#' # Create a branch that is going to be used for the worktree,
64+
#' # but don't check it out!
65+
#' git_branch_create(branch = "branch", checkout = FALSE, repo = repo)
66+
#'
67+
#' path <- tempfile("gert-examples-worktree")
68+
#'
69+
#' # Add a worktree for this branch
70+
#' git_worktree_add(
71+
#' name = "worktree",
72+
#' path = path,
73+
#' branch = "branch",
74+
#' repo = repo
75+
#' )
76+
#'
77+
#' # Worktree info
78+
#' git_worktree_list(repo = repo)
79+
#'
80+
#' # Note how the files are checked out here
81+
#' dir(path, all.files = TRUE)
82+
#'
83+
#' # And the branch that we are on at `path` is `"branch"`
84+
#' git_branch(repo = path)
85+
#'
86+
#' # Cleanup worktree, and the folder at `path`
87+
#' git_worktree_remove("worktree", repo = repo)
88+
#'
89+
#' # Cleanup repo
90+
#' unlink(repo, recursive = TRUE)
91+
NULL
92+
93+
#' @export
94+
#' @rdname git_worktree
95+
#' @useDynLib gert R_git_worktree_list
96+
git_worktree_list <- function(repo = '.') {
97+
repo <- git_open(repo)
98+
.Call(R_git_worktree_list, repo)
99+
}
100+
101+
#' @export
102+
#' @rdname git_worktree
103+
#' @useDynLib gert R_git_worktree_exists
104+
git_worktree_exists <- function(name, repo = '.') {
105+
repo <- git_open(repo)
106+
name <- as.character(name)
107+
.Call(R_git_worktree_exists, repo, name)
108+
}
109+
110+
#' @export
111+
#' @rdname git_worktree
112+
#' @useDynLib gert R_git_worktree_path
113+
git_worktree_path <- function(name, repo = ".") {
114+
repo <- git_open(repo)
115+
name <- as.character(name)
116+
.Call(R_git_worktree_path, repo, name)
117+
}
118+
119+
#' @export
120+
#' @rdname git_worktree
121+
#' @inheritParams git_branch
122+
#' @param path The path to checkout `branch` into. Importantly, the path up to
123+
#' the folder name must exist, but the folder name itself must not exist yet
124+
#' and will be created.
125+
#' @param branch The branch to checkout into `path`.
126+
#' @param lock Whether or not to lock the worktree on creation.
127+
#' @useDynLib gert R_git_worktree_add
128+
git_worktree_add <- function(
129+
name,
130+
path,
131+
branch,
132+
repo = ".",
133+
lock = FALSE,
134+
local = TRUE
135+
) {
136+
repo <- git_open(repo)
137+
name <- as.character(name)
138+
path <- normalizePath(path.expand(path), mustWork = FALSE)
139+
stopifnot(length(path) == 1L)
140+
# Avoid footguns where worktree metadata can get created, but branch can't be
141+
# checked out into the worktree, resulting in broken worktree state requiring
142+
# manual touching of `.git/` files
143+
if (dir.exists(path)) {
144+
stop(sprintf("Path at '%s' must not exist.", path))
145+
}
146+
if (!dir.exists(dirname(path))) {
147+
stop(sprintf("Path at '%s' must exist.", dirname(path)))
148+
}
149+
branch <- as.character(branch)
150+
lock <- as.logical(lock)
151+
if (!is.null(local)) {
152+
local <- as.logical(local)
153+
}
154+
.Call(R_git_worktree_add, repo, name, path, branch, lock, local)
155+
invisible()
156+
}
157+
158+
#' @export
159+
#' @rdname git_worktree
160+
git_worktree_remove <- function(name, repo = ".") {
161+
git_worktree_prune(
162+
name = name,
163+
repo = repo,
164+
prune_valid = TRUE,
165+
prune_locked = TRUE,
166+
prune_working_tree = TRUE
167+
)
168+
}
169+
170+
#' @export
171+
#' @rdname git_worktree
172+
#' @param prune_valid Whether or not to forcibly prune a _valid_ worktree.
173+
#' @param prune_locked Whether or not to forcibly prune a _locked_ worktree.
174+
#' @param prune_working_tree Whether or not to also remove the folder that the
175+
#' worktree was using, i.e. the `path` supplied to `git_worktree_add()`.
176+
#' @useDynLib gert R_git_worktree_prune
177+
git_worktree_prune <- function(
178+
name,
179+
repo = ".",
180+
prune_valid = FALSE,
181+
prune_locked = FALSE,
182+
prune_working_tree = FALSE
183+
) {
184+
repo <- git_open(repo)
185+
name <- as.character(name)
186+
prune_valid <- as.logical(prune_valid)
187+
prune_locked <- as.logical(prune_locked)
188+
prune_working_tree <- as.logical(prune_working_tree)
189+
.Call(
190+
R_git_worktree_prune,
191+
repo,
192+
name,
193+
prune_valid,
194+
prune_locked,
195+
prune_working_tree
196+
)
197+
invisible()
198+
}
199+
200+
#' @export
201+
#' @rdname git_worktree
202+
#' @useDynLib gert R_git_worktree_is_prunable
203+
git_worktree_is_prunable <- function(
204+
name,
205+
repo = ".",
206+
prune_valid = FALSE,
207+
prune_locked = FALSE
208+
) {
209+
repo <- git_open(repo)
210+
name <- as.character(name)
211+
prune_valid <- as.logical(prune_valid)
212+
prune_locked <- as.logical(prune_locked)
213+
.Call(
214+
R_git_worktree_is_prunable,
215+
repo,
216+
name,
217+
prune_valid,
218+
prune_locked
219+
)
220+
}
221+
222+
#' @export
223+
#' @rdname git_worktree
224+
#' @useDynLib gert R_git_worktree_lock
225+
git_worktree_lock <- function(name, repo = ".") {
226+
repo <- git_open(repo)
227+
name <- as.character(name)
228+
.Call(R_git_worktree_lock, repo, name)
229+
invisible()
230+
}
231+
232+
#' @export
233+
#' @rdname git_worktree
234+
#' @useDynLib gert R_git_worktree_unlock
235+
git_worktree_unlock <- function(name, repo = ".") {
236+
repo <- git_open(repo)
237+
name <- as.character(name)
238+
.Call(R_git_worktree_unlock, repo, name)
239+
invisible()
240+
}
241+
242+
#' @export
243+
#' @rdname git_worktree
244+
#' @useDynLib gert R_git_worktree_is_locked
245+
git_worktree_is_locked <- function(name, repo = ".") {
246+
repo <- git_open(repo)
247+
name <- as.character(name)
248+
.Call(R_git_worktree_is_locked, repo, name)
249+
}
250+
251+
#' @export
252+
#' @rdname git_worktree
253+
#' @useDynLib gert R_git_worktree_is_valid
254+
git_worktree_is_valid <- function(name, repo = ".") {
255+
repo <- git_open(repo)
256+
name <- as.character(name)
257+
.Call(R_git_worktree_is_valid, repo, name)
258+
}

man/git_archive.Rd

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/git_branch.Rd

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/git_commit.Rd

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/git_config.Rd

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/git_diff.Rd

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/git_fetch.Rd

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/git_ignore.Rd

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)