commit a129376b6f8adc074c4b53a4f78195ca32b78b1a from: Stefan Sperling date: Thu Mar 28 16:04:52 2019 UTC add initial implementation of 'got revert' commit - 95edb37e3f3bec85f054ff6fc44a9eca557e6080 commit + a129376b6f8adc074c4b53a4f78195ca32b78b1a blob - 265c9850b620a3a3ca13d48618052cfb435aadff blob + 785da132116932e9208b30119726b3a7885b8e0a --- got/got.1 +++ got/got.1 @@ -292,6 +292,19 @@ are as follows: .It Fl f Perform the operation even if the file contains uncommitted modifications. .El +.It Cm revert Ar file-path +Revert any uncommited changes in the file at the specified path. +File contents will be overwritten with those contained in the +work tree's base commit. There is no way to bring discarded +changes back after +.Cm got revert ! +.Pp +If the file was added with +.Cm got add +it will become an unversioned file again. +If the file was deleted with +.Cm got rm +it will be restored. .El .Sh EXIT STATUS .Ex -std got blob - 4a3c791020dff8d53212a4d0fac32cd6b5925676 blob + e083023d4b4d640205a1f9554968d287c356fdc3 --- got/got.c +++ got/got.c @@ -79,6 +79,7 @@ __dead static void usage_status(void); __dead static void usage_ref(void); __dead static void usage_add(void); __dead static void usage_rm(void); +__dead static void usage_revert(void); static const struct got_error* cmd_checkout(int, char *[]); static const struct got_error* cmd_update(int, char *[]); @@ -90,6 +91,7 @@ static const struct got_error* cmd_status(int, char * static const struct got_error* cmd_ref(int, char *[]); static const struct got_error* cmd_add(int, char *[]); static const struct got_error* cmd_rm(int, char *[]); +static const struct got_error* cmd_revert(int, char *[]); static struct cmd got_commands[] = { { "checkout", cmd_checkout, usage_checkout, @@ -112,6 +114,8 @@ static struct cmd got_commands[] = { "add a new file to version control" }, { "rm", cmd_rm, usage_rm, "remove a versioned file" }, + { "revert", cmd_revert, usage_revert, + "revert uncommitted changes" }, }; int @@ -1972,8 +1976,84 @@ cmd_rm(int argc, char *argv[]) error = got_worktree_schedule_delete(worktree, path, delete_local_mods, print_status, NULL, repo); + if (error) + goto done; +done: + if (repo) + got_repo_close(repo); + if (worktree) + got_worktree_close(worktree); + free(path); + free(cwd); + return error; +} + +__dead static void +usage_revert(void) +{ + fprintf(stderr, "usage: %s revert file-path\n", getprogname()); + exit(1); +} + +static void +revert_progress(void *arg, unsigned char status, const char *path) +{ + while (path[0] == '/') + path++; + printf("%c %s\n", status, path); +} + +static const struct got_error * +cmd_revert(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_worktree *worktree = NULL; + struct got_repository *repo = NULL; + char *cwd = NULL, *path = NULL; + int ch; + + while ((ch = getopt(argc, argv, "")) != -1) { + switch (ch) { + default: + usage_revert(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (argc != 1) + usage_revert(); + + path = realpath(argv[0], NULL); + if (path == NULL) { + error = got_error_from_errno(); + goto done; + } + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno(); + goto done; + } + error = got_worktree_open(&worktree, cwd); if (error) goto done; + + error = got_repo_open(&repo, got_worktree_get_repo_path(worktree)); + if (error != NULL) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 1, + got_worktree_get_root_path(worktree)); + if (error) + goto done; + + error = got_worktree_revert(worktree, path, + revert_progress, NULL, repo); + if (error) + goto done; done: if (repo) got_repo_close(repo); blob - 2bd95039582b9f7349099a9e19cd046c5028db04 blob + 6baad612e9f1ecbad710071e93668cb9e6da2c5b --- include/got_object.h +++ include/got_object.h @@ -153,6 +153,10 @@ void got_object_tree_close(struct got_tree_object *); const struct got_tree_entries *got_object_tree_get_entries( struct got_tree_object *); +/* Find a particular entry in a tree. */ +const struct got_tree_entry *got_object_tree_find_entry( + struct got_tree_object *, const char *); + /* * Compare two trees and indicate whether the entry at the specified path * differs between them. The path must not be the root path "/"; the function blob - d6cae67b59be7b305f805fc6a0b6590bff63815c blob + d49c5f89501111450660c1051f14f61b0ce3dd09 --- include/got_worktree.h +++ include/got_worktree.h @@ -28,6 +28,7 @@ struct got_worktree; #define GOT_STATUS_MISSING '!' #define GOT_STATUS_UNVERSIONED '?' #define GOT_STATUS_OBSTRUCTED '~' +#define GOT_STATUS_REVERT 'R' /* * Attempt to initialize a new work tree on disk. @@ -109,7 +110,7 @@ typedef const struct got_error *(*got_worktree_cancel_ * void * argument, and the path of each checked out file. */ const struct got_error *got_worktree_checkout_files(struct got_worktree *, - struct got_repository *, got_worktree_checkout_cb progress, void *, + struct got_repository *, got_worktree_checkout_cb, void *, got_worktree_cancel_cb, void *); /* A callback function which is invoked to report a path's status. */ @@ -144,3 +145,10 @@ const struct got_error *got_worktree_schedule_add(stru const struct got_error * got_worktree_schedule_delete(struct got_worktree *, const char *, int, got_worktree_status_cb, void *, struct got_repository *); + +/* + * Revert a file at the specified path such that it matches its + * original state in the worktree's base commit. + */ +const struct got_error *got_worktree_revert(struct got_worktree *, + const char *, got_worktree_checkout_cb, void *, struct got_repository *); blob - 911aa0e314aa74b4cbc6a5f1f749500f7cd6eb3e blob + eb8980676a00c3a6d06cb7825b9821b9828b23f0 --- lib/object.c +++ lib/object.c @@ -1352,6 +1352,12 @@ find_entry_by_name(struct got_tree_object *tree, const return te; } return NULL; +} + +const struct got_tree_entry * +got_object_tree_find_entry(struct got_tree_object *tree, const char *name) +{ + return find_entry_by_name(tree, name, strlen(name)); } const struct got_error * blob - 487e120c8cd12a0a3b7d5ec4f6272064cb3a5f26 blob + 917cd5d8b0d2657829ef7fa395ed2320226cffaa --- lib/path.c +++ lib/path.c @@ -151,6 +151,12 @@ int got_path_is_root_dir(const char *path) { return (path[0] == '/' && path[1] == '\0'); +} + +int +got_path_is_current_dir(const char *path) +{ + return (path[0] == '.' && path[1] == '\0'); } int blob - 694339afe5680f6668ebb29a16409b9056d82593 blob + 106aa6ffdc6f810cb3b0515b4e20cb1a875123ed --- lib/worktree.c +++ lib/worktree.c @@ -871,8 +871,8 @@ static const struct got_error * install_blob(struct got_worktree *worktree, const char *ondisk_path, const char *path, uint16_t te_mode, uint16_t st_mode, struct got_blob_object *blob, int restoring_missing_file, - struct got_repository *repo, got_worktree_checkout_cb progress_cb, - void *progress_arg) + int reverting_versioned_file, struct got_repository *repo, + got_worktree_checkout_cb progress_cb, void *progress_arg) { const struct got_error *err = NULL; int fd = -1; @@ -913,6 +913,8 @@ install_blob(struct got_worktree *worktree, const char if (restoring_missing_file) (*progress_cb)(progress_arg, GOT_STATUS_MISSING, path); + else if (reverting_versioned_file) + (*progress_cb)(progress_arg, GOT_STATUS_REVERT, path); else (*progress_cb)(progress_arg, update ? GOT_STATUS_UPDATE : GOT_STATUS_ADD, path); @@ -1170,8 +1172,8 @@ update_blob(struct got_worktree *worktree, goto done; } else { err = install_blob(worktree, ondisk_path, path, te->mode, - sb.st_mode, blob, status == GOT_STATUS_MISSING, repo, - progress_cb, progress_arg); + sb.st_mode, blob, status == GOT_STATUS_MISSING, 0, + repo, progress_cb, progress_arg); if (err) goto done; err = update_blob_fileindex_entry(worktree, fileindex, ie, @@ -1924,3 +1926,184 @@ done: err = unlockerr; return err; } + +const struct got_error * +got_worktree_revert(struct got_worktree *worktree, + const char *ondisk_path, + got_worktree_checkout_cb progress_cb, void *progress_arg, + struct got_repository *repo) +{ + struct got_fileindex *fileindex = NULL; + struct got_fileindex_entry *ie = NULL; + char *relpath, *fileindex_path = NULL, *new_fileindex_path = NULL; + char *tree_path = NULL, *parent_path, *te_name; + FILE *index = NULL, *new_index = NULL; + const struct got_error *err = NULL, *unlockerr = NULL; + struct got_tree_object *tree = NULL; + struct got_object_id id, *tree_id = NULL; + const struct got_tree_entry *te; + struct got_blob_object *blob = NULL; + unsigned char status; + struct stat sb; + + err = lock_worktree(worktree, LOCK_EX); + if (err) + return err; + + err = got_path_skip_common_ancestor(&relpath, + got_worktree_get_root_path(worktree), ondisk_path); + if (err) + goto done; + + fileindex = got_fileindex_alloc(); + if (fileindex == NULL) { + err = got_error_from_errno(); + goto done; + } + + if (asprintf(&fileindex_path, "%s/%s/%s", worktree->root_path, + GOT_WORKTREE_GOT_DIR, GOT_WORKTREE_FILE_INDEX) == -1) { + err = got_error_from_errno(); + fileindex_path = NULL; + goto done; + } + + index = fopen(fileindex_path, "rb"); + if (index == NULL) { + err = got_error_from_errno(); + goto done; + } + + err = got_fileindex_read(fileindex, index); + if (err) + goto done; + + ie = got_fileindex_entry_get(fileindex, relpath); + if (ie == NULL) { + err = got_error(GOT_ERR_BAD_PATH); + goto done; + } + + /* Construct in-repository path of tree which contains this blob. */ + err = got_path_dirname(&parent_path, ie->path); + if (err) { + if (err->code != GOT_ERR_BAD_PATH) + goto done; + parent_path = "/"; + } + if (got_path_is_root_dir(worktree->path_prefix)) { + tree_path = strdup(parent_path); + if (tree_path == NULL) { + err = got_error_from_errno(); + goto done; + } + } else { + if (got_path_is_root_dir(parent_path)) { + tree_path = strdup(worktree->path_prefix); + if (tree_path == NULL) { + err = got_error_from_errno(); + goto done; + } + } else { + if (asprintf(&tree_path, "%s/%s", + worktree->path_prefix, parent_path) == -1) { + err = got_error_from_errno(); + goto done; + } + } + } + + err = got_object_id_by_path(&tree_id, repo, worktree->base_commit_id, + tree_path); + if (err) + goto done; + + err = got_object_open_as_tree(&tree, repo, tree_id); + if (err) + goto done; + + te_name = basename(ie->path); + if (te_name == NULL) { + err = got_error_from_errno(); + goto done; + } + + err = get_file_status(&status, &sb, ie, ondisk_path, repo); + if (err) + goto done; + + te = got_object_tree_find_entry(tree, te_name); + if (te == NULL && status != GOT_STATUS_ADD) { + err = got_error(GOT_ERR_NO_TREE_ENTRY); + goto done; + } + + switch (status) { + case GOT_STATUS_ADD: + (*progress_cb)(progress_arg, GOT_STATUS_REVERT, ie->path); + got_fileindex_entry_remove(fileindex, ie); + break; + case GOT_STATUS_DELETE: + case GOT_STATUS_MODIFY: + case GOT_STATUS_CONFLICT: + case GOT_STATUS_MISSING: + memcpy(id.sha1, ie->blob_sha1, SHA1_DIGEST_LENGTH); + err = got_object_open_as_blob(&blob, repo, &id, 8192); + if (err) + goto done; + err = install_blob(worktree, ondisk_path, ie->path, + te->mode, sb.st_mode, blob, 0, 1, repo, progress_cb, + progress_arg); + if (err) + goto done; + if (status == GOT_STATUS_DELETE) { + err = update_blob_fileindex_entry(worktree, + fileindex, ie, ondisk_path, ie->path, blob, 1); + if (err) + goto done; + } + break; + default: + goto done; + } + + err = got_opentemp_named(&new_fileindex_path, &new_index, + fileindex_path); + if (err) + goto done; + + err = got_fileindex_write(fileindex, new_index); + if (err) + goto done; + + if (rename(new_fileindex_path, fileindex_path) != 0) { + err = got_error_from_errno(); + goto done; + } + + free(new_fileindex_path); + new_fileindex_path = NULL; +done: + free(relpath); + free(tree_path); + if (blob) + got_object_blob_close(blob); + if (tree) + got_object_tree_close(tree); + free(tree_id); + if (index) { + if (fclose(index) != 0 && err == NULL) + err = got_error_from_errno(); + } + if (new_fileindex_path) { + if (unlink(new_fileindex_path) != 0 && err == NULL) + err = got_error_from_errno(); + free(new_fileindex_path); + } + if (fileindex) + got_fileindex_free(fileindex); + unlockerr = lock_worktree(worktree, LOCK_SH); + if (unlockerr && err == NULL) + err = unlockerr; + return err; +} blob - /dev/null blob + 4c59fbdebed9af30db2c55f99c3b569c552e8c31 (mode 755) --- /dev/null +++ regress/cmdline/revert.sh @@ -0,0 +1,140 @@ +#!/bin/sh +# +# Copyright (c) 2019 Stefan Sperling +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +. ./common.sh + +function test_revert_basic { + local testroot=`test_init revert_basic` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + echo "modified alpha" > $testroot/wt/alpha + + echo 'R alpha' > $testroot/stdout.expected + + (cd $testroot/wt && got revert alpha > $testroot/stdout) + + cmp $testroot/stdout.expected $testroot/stdout + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + echo "alpha" > $testroot/content.expected + cat $testroot/wt/alpha > $testroot/content + + cmp $testroot/content.expected $testroot/content + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/content.expected $testroot/content + fi + test_done "$testroot" "$ret" + +} + +function test_revert_rm { + local testroot=`test_init revert_rm` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + (cd $testroot/wt && got rm beta >/dev/null) + + echo 'R beta' > $testroot/stdout.expected + + (cd $testroot/wt && got revert beta > $testroot/stdout) + + cmp $testroot/stdout.expected $testroot/stdout + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + echo "beta" > $testroot/content.expected + cat $testroot/wt/beta > $testroot/content + + cmp $testroot/content.expected $testroot/content + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/content.expected $testroot/content + fi + test_done "$testroot" "$ret" +} + +function test_revert_add { + local testroot=`test_init revert_add` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + echo "new file" > $testroot/wt/new + (cd $testroot/wt && got add new >/dev/null) + + echo 'R new' > $testroot/stdout.expected + + (cd $testroot/wt && got revert new > $testroot/stdout) + + cmp $testroot/stdout.expected $testroot/stdout + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + echo "new file" > $testroot/content.expected + cat $testroot/wt/new > $testroot/content + + cmp $testroot/content.expected $testroot/content + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/content.expected $testroot/content + test_done "$testroot" "$ret" + return 1 + fi + + echo '? new' > $testroot/stdout.expected + + (cd $testroot/wt && got status > $testroot/stdout) + + cmp $testroot/stdout.expected $testroot/stdout + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + fi + test_done "$testroot" "$ret" +} + +run_test test_revert_basic +run_test test_revert_rm +run_test test_revert_add