diff --git a/NAMESPACE b/NAMESPACE index f1c504a..21203e9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -66,6 +66,7 @@ export(git_remote_set_url) export(git_reset_hard) export(git_reset_mixed) export(git_reset_soft) +export(git_restore) export(git_revert) export(git_rm) export(git_signature) @@ -158,6 +159,7 @@ useDynLib(gert,R_git_repository_open) useDynLib(gert,R_git_repository_path) useDynLib(gert,R_git_repository_rm) useDynLib(gert,R_git_reset) +useDynLib(gert,R_git_restore) useDynLib(gert,R_git_revert) useDynLib(gert,R_git_signature_create) useDynLib(gert,R_git_signature_default) diff --git a/NEWS.md b/NEWS.md index f05fa8b..7a006cf 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # gert (development version) +- Add `git_restore()` function (#259) - Add `git_config_get()`, `git_config_local_get()`, and `git_config_global_get()` to retrieve a single named config option, returning `NULL` if unset (#267). - `git_clone()` without a `path` argument now clones into a directory named after the "humanish" part of the URL, so "git@github.com:francisbarton/myrepo.git" gets cloned into `myrepo` (@francisbardon, #192). diff --git a/R/commit.R b/R/commit.R index d09303e..e8676f2 100644 --- a/R/commit.R +++ b/R/commit.R @@ -322,22 +322,7 @@ git_revert <- function( assert_string(ref) stopifnot(is.logical(commit), length(commit) == 1) - sha <- tryCatch(git_commit_id(ref, repo = repo), error = function(e) { - stop(sprintf( - "Can't find reference/commit '%s' in the current branch history", - ref - )) - }) - - head_sha <- git_commit_id("HEAD", repo = repo) - sha_descends_from_head <- git_commit_descendant_of( - ancestor = sha, - ref = "HEAD", - repo = repo - ) - if (sha != head_sha && !sha_descends_from_head) { - stop(sprintf("commit '%s' is not in the current branch history", ref)) - } + sha <- check_ref_in_history(ref, repo) .Call(R_git_revert, repo, sha) diff --git a/R/restore.R b/R/restore.R new file mode 100644 index 0000000..e003d29 --- /dev/null +++ b/R/restore.R @@ -0,0 +1,58 @@ +#' Restore working tree files +#' +#' Restores specified paths in the working tree from a given ref, equivalent +#' to `git restore --source= ` (or the older +#' `git checkout -- `). The ref must be reachable from the +#' current HEAD. By default restores from HEAD, discarding any local +#' modifications. +#' +#' @export +#' @name git_restore +#' @rdname git_restore +#' @family git +#' @inheritParams git_open +#' @param path character vector with file paths to restore, relative to the +#' repository root. Use `"."` to restore all tracked files. +#' @param ref revision string with a branch/tag/commit to restore from. +#' Defaults to `"HEAD"`. +#' @return Invisibly, the [git_status()] after restoring. +#' @examplesIf interactive() +#' repo <- file.path(tempdir(), "myrepo") +#' git_init(repo) +#' +#' # Set a user if no default +#' if (!user_is_configured()) { +#' git_config_set("user.name", "Jerry") +#' git_config_set("user.email", "jerry@gmail.com") +#' } +#' +#' writeLines("hello", file.path(repo, "hello.txt")) +#' git_add("hello.txt", repo = repo) +#' git_commit("First commit", repo = repo) +#' +#' # Modify the file, then restore it from HEAD +#' writeLines("oops", file.path(repo, "hello.txt")) +#' git_restore("hello.txt", repo = repo) +#' readLines(file.path(repo, "hello.txt")) # "hello" +#' +#' unlink(repo, recursive = TRUE) +#' @useDynLib gert R_git_restore +git_restore <- function(path, ref = "HEAD", repo = ".") { + repo <- git_open(repo) + path <- check_path_tracked(as.character(path), repo) + ref <- check_ref_in_history(ref, repo) + .Call(R_git_restore, repo, path, ref) + invisible(git_status(repo = repo)) +} + +check_path_tracked <- function(path, repo) { + if (identical(path, ".")) { + return(character(0)) + } + tracked <- git_ls(repo = repo)$path + untracked <- setdiff(path, tracked) + if (length(untracked) > 0) { + stop("Path(s) not tracked by git: ", toString(untracked)) + } + path +} diff --git a/R/utils-sha.R b/R/utils-sha.R new file mode 100644 index 0000000..e1202ea --- /dev/null +++ b/R/utils-sha.R @@ -0,0 +1,17 @@ +check_ref_in_history <- function(ref, repo) { + sha <- tryCatch(git_commit_id(ref, repo = repo), error = function(e) { + stop(sprintf("Cannot resolve '%s' to a commit", ref)) + }) + + head_sha <- git_commit_id("HEAD", repo = repo) + sha_descends_from_head <- git_commit_descendant_of( + ancestor = sha, + ref = "HEAD", + repo = repo + ) + if (sha != head_sha && !sha_descends_from_head) { + stop(sprintf("'%s' is not in the current branch history", ref)) + } + + sha +} diff --git a/man/git_archive.Rd b/man/git_archive.Rd index d9574a1..df1a534 100644 --- a/man/git_archive.Rd +++ b/man/git_archive.Rd @@ -38,6 +38,7 @@ Other git: \code{\link{git_remote}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, diff --git a/man/git_branch.Rd b/man/git_branch.Rd index 661a453..9d394bc 100644 --- a/man/git_branch.Rd +++ b/man/git_branch.Rd @@ -80,6 +80,7 @@ Other git: \code{\link{git_remote}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, diff --git a/man/git_commit.Rd b/man/git_commit.Rd index 4449a17..624fe86 100644 --- a/man/git_commit.Rd +++ b/man/git_commit.Rd @@ -111,6 +111,7 @@ Other git: \code{\link{git_remote}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, diff --git a/man/git_config.Rd b/man/git_config.Rd index 8361d55..fac4dda 100644 --- a/man/git_config.Rd +++ b/man/git_config.Rd @@ -118,6 +118,7 @@ Other git: \code{\link{git_remote}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, diff --git a/man/git_diff.Rd b/man/git_diff.Rd index 8a24354..4f8938b 100644 --- a/man/git_diff.Rd +++ b/man/git_diff.Rd @@ -46,6 +46,7 @@ Other git: \code{\link{git_remote}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, diff --git a/man/git_fetch.Rd b/man/git_fetch.Rd index 448b18c..c8cdc2e 100644 --- a/man/git_fetch.Rd +++ b/man/git_fetch.Rd @@ -158,6 +158,7 @@ Other git: \code{\link{git_remote}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, diff --git a/man/git_history.Rd b/man/git_history.Rd index fdf2b8f..6e5c0d9 100644 --- a/man/git_history.Rd +++ b/man/git_history.Rd @@ -67,6 +67,7 @@ Other git: \code{\link{git_remote}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, diff --git a/man/git_ignore.Rd b/man/git_ignore.Rd index 4ffedb7..2f65136 100644 --- a/man/git_ignore.Rd +++ b/man/git_ignore.Rd @@ -39,6 +39,7 @@ Other git: \code{\link{git_remote}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, diff --git a/man/git_merge.Rd b/man/git_merge.Rd index c6386c5..cafabec 100644 --- a/man/git_merge.Rd +++ b/man/git_merge.Rd @@ -77,6 +77,7 @@ Other git: \code{\link{git_remote}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, diff --git a/man/git_rebase.Rd b/man/git_rebase.Rd index 620b62d..1908d97 100644 --- a/man/git_rebase.Rd +++ b/man/git_rebase.Rd @@ -66,6 +66,7 @@ Other git: \code{\link{git_remote}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, diff --git a/man/git_remote.Rd b/man/git_remote.Rd index aca4880..77a2bff 100644 --- a/man/git_remote.Rd +++ b/man/git_remote.Rd @@ -61,6 +61,7 @@ Other git: \code{\link[=git_rebase]{git_rebase()}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, diff --git a/man/git_repo.Rd b/man/git_repo.Rd index d4515b2..053b00b 100644 --- a/man/git_repo.Rd +++ b/man/git_repo.Rd @@ -96,6 +96,7 @@ Other git: \code{\link[=git_rebase]{git_rebase()}}, \code{\link{git_remote}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, diff --git a/man/git_reset.Rd b/man/git_reset.Rd index 1137a08..21ea038 100644 --- a/man/git_reset.Rd +++ b/man/git_reset.Rd @@ -43,6 +43,7 @@ Other git: \code{\link[=git_rebase]{git_rebase()}}, \code{\link{git_remote}}, \code{\link{git_repo}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, diff --git a/man/git_restore.Rd b/man/git_restore.Rd new file mode 100644 index 0000000..21cb804 --- /dev/null +++ b/man/git_restore.Rd @@ -0,0 +1,76 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/restore.R +\name{git_restore} +\alias{git_restore} +\title{Restore working tree files} +\usage{ +git_restore(path, ref = "HEAD", repo = ".") +} +\arguments{ +\item{path}{character vector with file paths to restore, relative to the +repository root. Use \code{"."} to restore all tracked files.} + +\item{ref}{revision string with a branch/tag/commit to restore from. +Defaults to \code{"HEAD"}.} + +\item{repo}{The path to the git repository. If the directory is not a +repository, parent directories are considered (see \code{\link[=git_find]{git_find()}}). To disable +this search, provide the filepath protected with \code{\link[=I]{I()}}. When using this +parameter, always explicitly call by name (i.e. \verb{repo = }) because future +versions of gert may have additional parameters.} +} +\value{ +Invisibly, the \code{\link[=git_status]{git_status()}} after restoring. +} +\description{ +Restores specified paths in the working tree from a given ref, equivalent +to \verb{git restore --source= } (or the older +\verb{git checkout -- }). The ref must be reachable from the +current HEAD. By default restores from HEAD, discarding any local +modifications. +} +\examples{ +\dontshow{if (interactive()) withAutoprint(\{ # examplesIf} +repo <- file.path(tempdir(), "myrepo") +git_init(repo) + +# Set a user if no default +if (!user_is_configured()) { + git_config_set("user.name", "Jerry") + git_config_set("user.email", "jerry@gmail.com") +} + +writeLines("hello", file.path(repo, "hello.txt")) +git_add("hello.txt", repo = repo) +git_commit("First commit", repo = repo) + +# Modify the file, then restore it from HEAD +writeLines("oops", file.path(repo, "hello.txt")) +git_restore("hello.txt", repo = repo) +readLines(file.path(repo, "hello.txt")) # "hello" + +unlink(repo, recursive = TRUE) +\dontshow{\}) # examplesIf} +} +\seealso{ +Other git: +\code{\link{git_archive}}, +\code{\link[=git_branch]{git_branch()}}, +\code{\link[=git_commit]{git_commit()}}, +\code{\link[=git_config]{git_config()}}, +\code{\link[=git_diff]{git_diff()}}, +\code{\link[=git_fetch]{git_fetch()}}, +\code{\link{git_history}}, +\code{\link{git_ignore}}, +\code{\link[=git_merge]{git_merge()}}, +\code{\link[=git_rebase]{git_rebase()}}, +\code{\link{git_remote}}, +\code{\link{git_repo}}, +\code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_revert]{git_revert()}}, +\code{\link[=git_signature]{git_signature()}}, +\code{\link{git_stash}}, +\code{\link{git_tag}}, +\code{\link{git_worktree}} +} +\concept{git} diff --git a/man/git_revert.Rd b/man/git_revert.Rd index 85a9568..713685d 100644 --- a/man/git_revert.Rd +++ b/man/git_revert.Rd @@ -88,6 +88,7 @@ Other git: \code{\link{git_remote}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, \code{\link{git_tag}}, diff --git a/man/git_signature.Rd b/man/git_signature.Rd index c1a40c4..e742ba8 100644 --- a/man/git_signature.Rd +++ b/man/git_signature.Rd @@ -67,6 +67,7 @@ Other git: \code{\link{git_remote}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link{git_stash}}, \code{\link{git_tag}}, diff --git a/man/git_stash.Rd b/man/git_stash.Rd index 5d1ba3f..0d5a761 100644 --- a/man/git_stash.Rd +++ b/man/git_stash.Rd @@ -61,6 +61,7 @@ Other git: \code{\link{git_remote}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_tag}}, diff --git a/man/git_tag.Rd b/man/git_tag.Rd index 94c0d14..9556212 100644 --- a/man/git_tag.Rd +++ b/man/git_tag.Rd @@ -51,6 +51,7 @@ Other git: \code{\link{git_remote}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, diff --git a/man/git_worktree.Rd b/man/git_worktree.Rd index 36a6762..620092c 100644 --- a/man/git_worktree.Rd +++ b/man/git_worktree.Rd @@ -173,6 +173,7 @@ Other git: \code{\link{git_remote}}, \code{\link{git_repo}}, \code{\link[=git_reset]{git_reset()}}, +\code{\link[=git_restore]{git_restore()}}, \code{\link[=git_revert]{git_revert()}}, \code{\link[=git_signature]{git_signature()}}, \code{\link{git_stash}}, diff --git a/src/files.c b/src/files.c index 597980c..98cc911 100644 --- a/src/files.c +++ b/src/files.c @@ -211,3 +211,17 @@ SEXP R_git_status_list(SEXP ptr, SEXP show_staged, SEXP path_spec){ UNPROTECT(3); return out; } + +SEXP R_git_restore(SEXP ptr, SEXP files, SEXP ref){ + git_repository *repo = get_git_repository(ptr); + git_object *obj = resolve_refish(ref, repo); + git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT; + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + git_strarray *paths = files_to_array(files); + opts.paths = *paths; + int err = git_checkout_tree(repo, obj, &opts); + git_strarray_free(paths); + git_object_free(obj); + bail_if(err, "git_checkout_tree"); + return R_NilValue; +} diff --git a/src/init.c b/src/init.c index a167a8d..5703c2d 100644 --- a/src/init.c +++ b/src/init.c @@ -52,6 +52,7 @@ extern SEXP R_git_remote_push(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP R_git_remote_refspecs(SEXP, SEXP); extern SEXP R_git_remote_remove(SEXP, SEXP); extern SEXP R_git_remote_set_url(SEXP, SEXP, SEXP); +extern SEXP R_git_restore(SEXP, SEXP, SEXP); extern SEXP R_git_revert(SEXP, SEXP); extern SEXP R_git_repository_add(SEXP, SEXP, SEXP); extern SEXP R_git_repository_clone(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); @@ -146,6 +147,7 @@ static const R_CallMethodDef CallEntries[] = { {"R_git_repository_path", (DL_FUNC) &R_git_repository_path, 1}, {"R_git_repository_rm", (DL_FUNC) &R_git_repository_rm, 2}, {"R_git_reset", (DL_FUNC) &R_git_reset, 3}, + {"R_git_restore", (DL_FUNC) &R_git_restore, 3}, {"R_git_revert", (DL_FUNC) &R_git_revert, 2}, {"R_git_signature_create", (DL_FUNC) &R_git_signature_create, 4}, {"R_git_signature_default", (DL_FUNC) &R_git_signature_default, 1}, diff --git a/tests/testthat/test-restore.R b/tests/testthat/test-restore.R new file mode 100644 index 0000000..7625a4c --- /dev/null +++ b/tests/testthat/test-restore.R @@ -0,0 +1,84 @@ +test_that("git_restore restores a modified file from HEAD", { + repo <- git_init(tempfile("gert-tests-restore")) + on.exit(unlink(repo, recursive = TRUE)) + configure_local_user(repo) + + writeLines("original", file.path(repo, "hello.txt")) + git_add("hello.txt", repo = repo) + git_commit("First commit", repo = repo) + + writeLines("modified", file.path(repo, "hello.txt")) + expect_equal(readLines(file.path(repo, "hello.txt")), "modified") + + git_restore("hello.txt", repo = repo) + expect_equal(readLines(file.path(repo, "hello.txt")), "original") +}) + +test_that("git_restore restores from a specific ref", { + repo <- git_init(tempfile("gert-tests-restore-ref")) + on.exit(unlink(repo, recursive = TRUE)) + configure_local_user(repo) + + writeLines("v1", file.path(repo, "hello.txt")) + writeLines("v1", file.path(repo, "keep.txt")) + writeLines("v1", file.path(repo, "wip.txt")) + writeLines("v1", file.path(repo, "untracked.txt")) + + git_add(c("hello.txt", "keep.txt", "wip.txt"), repo = repo) + first <- git_commit("First commit", repo = repo) + + writeLines("v2", file.path(repo, "hello.txt")) + writeLines("v2", file.path(repo, "keep.txt")) + writeLines("v2", file.path(repo, "wip.txt")) + git_add(c("hello.txt", "keep.txt"), repo = repo) + git_commit("Second commit", repo = repo) + + git_restore("hello.txt", ref = first, repo = repo) + expect_equal(readLines(file.path(repo, "hello.txt")), "v1") + expect_equal(readLines(file.path(repo, "keep.txt")), "v2") + expect_equal(readLines(file.path(repo, "wip.txt")), "v2") + expect_equal(readLines(file.path(repo, "untracked.txt")), "v1") + +}) + +test_that("git_restore restores all files with path = '.'", { + repo <- git_init(tempfile("gert-tests-restore-dot")) + on.exit(unlink(repo, recursive = TRUE)) + configure_local_user(repo) + + writeLines("original a", file.path(repo, "a.txt")) + writeLines("original b", file.path(repo, "b.txt")) + git_add(c("a.txt", "b.txt"), repo = repo) + git_commit("First commit", repo = repo) + + writeLines("modified a", file.path(repo, "a.txt")) + writeLines("modified b", file.path(repo, "b.txt")) + + git_restore(".", repo = repo) + expect_equal(readLines(file.path(repo, "a.txt")), "original a") + expect_equal(readLines(file.path(repo, "b.txt")), "original b") +}) + +test_that("git_restore raises an error for an untracked path", { + repo <- git_init(tempfile("gert-tests-restore-untracked")) + on.exit(unlink(repo, recursive = TRUE)) + configure_local_user(repo) + + writeLines("hello", file.path(repo, "hello.txt")) + git_add("hello.txt", repo = repo) + git_commit("First commit", repo = repo) + + expect_error(git_restore("nottracked.txt", repo = repo), "not tracked by git") +}) + +test_that("git_restore raises an error for an invalid ref", { + repo <- git_init(tempfile("gert-tests-restore-invalid")) + on.exit(unlink(repo, recursive = TRUE)) + configure_local_user(repo) + + writeLines("hello", file.path(repo, "hello.txt")) + git_add("hello.txt", repo = repo) + git_commit("First commit", repo = repo) + + expect_error(git_restore("hello.txt", ref = "notaref", repo = repo), "notaref") +})