Skip to content

Commit f26d505

Browse files
committed
feat: add git_revert()
Fix r-lib#206 Co-Authored-By: Claude Opus 4.6
1 parent 9b74e71 commit f26d505

24 files changed

Lines changed: 331 additions & 1 deletion

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export(git_remote_set_url)
6060
export(git_reset_hard)
6161
export(git_reset_mixed)
6262
export(git_reset_soft)
63+
export(git_revert)
6364
export(git_rm)
6465
export(git_signature)
6566
export(git_signature_default)
@@ -152,6 +153,7 @@ useDynLib(gert,R_git_repository_open)
152153
useDynLib(gert,R_git_repository_path)
153154
useDynLib(gert,R_git_repository_rm)
154155
useDynLib(gert,R_git_reset)
156+
useDynLib(gert,R_git_revert)
155157
useDynLib(gert,R_git_signature_create)
156158
useDynLib(gert,R_git_signature_default)
157159
useDynLib(gert,R_git_signature_parse)

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
2.3.1
2+
- Add git_revert() function
23
- git_stat_files() gains a 'max' parameter
34
- Fix function declaration error
45
2.3.0

R/commit.R

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,13 @@ git_ls <- function(repo = '.', ref = NULL) {
212212
#' @param after date or timestamp: only include commits starting this date
213213
#' @param path character vector with paths to filter on; only commits that
214214
#' touch these paths are included
215-
git_log <- function(ref = "HEAD", max = 100, after = NULL, path = NULL, repo = ".") {
215+
git_log <- function(
216+
ref = "HEAD",
217+
max = 100,
218+
after = NULL,
219+
path = NULL,
220+
repo = "."
221+
) {
216222
repo <- git_open(repo)
217223
ref <- as.character(ref)
218224
max <- as.integer(max)
@@ -233,6 +239,120 @@ git_stat_files <- function(files, ref = "HEAD", max = NULL, repo = '.') {
233239
.Call(R_git_stat_files, repo, files, ref, max)
234240
}
235241

242+
#' Revert a commit
243+
#'
244+
#' Applies the inverse of the changes introduced by a given commit, equivalent
245+
#' to `git revert <commit>`. The commit must be reachable from the current HEAD.
246+
#'
247+
#' By default, a new revert commit is created immediately. Set `no_commit = TRUE`
248+
#' to only stage the reverted changes without committing, leaving you free to
249+
#' amend or combine them before calling [git_commit()].
250+
#'
251+
#' @export
252+
#' @name git_revert
253+
#' @rdname git_revert
254+
#' @family git
255+
#' @inheritParams git_open
256+
#' @inheritParams git_commit
257+
#' @param commit a commit reference (SHA, branch name, or revision expression
258+
#' such as `HEAD~1`) identifying the commit to revert. Must be an ancestor of
259+
#' the current HEAD.
260+
#' @param no_commit if `TRUE`, stage the reverted changes without creating a
261+
#' commit. Default is `FALSE`, that is to say, by default a commit is made.
262+
#' @param message a commit message. Similar default to `git revert`.
263+
#' @return The SHA of the new revert commit (invisibly), or `NULL` when
264+
#' `no_commit = TRUE`.
265+
#' @examples
266+
#' repo <- file.path(tempdir(), "myrepo")
267+
#' git_init(repo)
268+
#'
269+
#' # Set a user if no default
270+
#' if (!user_is_configured()) {
271+
#' git_config_set("user.name", "Jerry")
272+
#' git_config_set("user.email", "jerry@gmail.com")
273+
#' }
274+
#'
275+
#' writeLines("hello", file.path(repo, "hello.txt"))
276+
#' git_add("hello.txt", repo = repo)
277+
#' git_commit("First commit", repo = repo)
278+
#'
279+
#' writeLines("world", file.path(repo, "hello.txt"))
280+
#' git_add("hello.txt", repo = repo)
281+
#' bad_commit <- git_commit("Second commit", repo = repo)
282+
#'
283+
#' # Default: revert and commit with an auto-generated message
284+
#' git_revert(bad_commit, repo = repo)
285+
#' git_log(repo = repo)
286+
#'
287+
#' # Revert with a custom message
288+
#' writeLines("oops", file.path(repo, "hello.txt"))
289+
#' git_add("hello.txt", repo = repo)
290+
#' bad_commit2 <- git_commit("Third commit", repo = repo)
291+
#' git_revert(bad_commit2, message = "Undo third commit\n", repo = repo)
292+
#' git_log(repo = repo)
293+
#'
294+
#' # Stage the revert without committing
295+
#' writeLines("again", file.path(repo, "hello.txt"))
296+
#' git_add("hello.txt", repo = repo)
297+
#' bad_commit3 <- git_commit("Fourth commit", repo = repo)
298+
#' git_revert(bad_commit3, no_commit = TRUE, repo = repo)
299+
#' git_status(repo = repo)
300+
#'
301+
#' unlink(repo, recursive = TRUE)
302+
#' @useDynLib gert R_git_revert
303+
git_revert <- function(
304+
commit,
305+
message = NULL,
306+
author = NULL,
307+
committer = NULL,
308+
no_commit = FALSE,
309+
repo = '.'
310+
) {
311+
repo <- git_open(repo)
312+
assert_string(commit)
313+
stopifnot(is.logical(no_commit), length(no_commit) == 1)
314+
315+
sha <- try(git_commit_id(commit, repo = repo), silent = TRUE)
316+
if (inherits(sha, "try-error")) {
317+
stop(sprintf(
318+
"Can't find commit '%s' in the current branch history",
319+
commit
320+
))
321+
}
322+
323+
head_sha <- git_commit_id("HEAD", repo = repo)
324+
sha_descends_from_head <- git_commit_descendant_of(
325+
ancestor = sha,
326+
ref = "HEAD",
327+
repo = repo
328+
)
329+
if (sha != head_sha && !sha_descends_from_head) {
330+
stop(sprintf("commit '%s' is not in the current branch history", commit))
331+
}
332+
333+
.Call(R_git_revert, repo, sha)
334+
335+
if (no_commit) {
336+
return(NULL)
337+
}
338+
if (!length(message)) {
339+
info <- git_commit_info(sha, repo = repo)
340+
subject <- strsplit(info$message, "\n")[[1]][1]
341+
subject <- sub("\\s+$", "", subject)
342+
message <- sprintf(
343+
'Revert "%s"\n\nThis reverts commit %s.\n',
344+
subject,
345+
info$id
346+
)
347+
}
348+
invisible(git_commit(
349+
message,
350+
author = author,
351+
committer = committer,
352+
repo = repo
353+
))
354+
}
355+
236356
assert_string <- function(x) {
237357
if (!is.character(x) || !length(x)) {
238358
stop("Argument must be a string of length 1")

R/rebase.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ git_cherry_pick <- function(commit, repo = '.') {
105105
.Call(R_git_cherry_pick, repo, commit)
106106
}
107107

108+
108109
#' @export
109110
#' @rdname git_rebase
110111
#' @useDynLib gert R_git_ahead_behind

man/git_archive.Rd

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

man/git_branch.Rd

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

man/git_commit.Rd

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

man/git_config.Rd

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

man/git_diff.Rd

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

man/git_fetch.Rd

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

0 commit comments

Comments
 (0)