Commit Diff


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 <stsp@openbsd.org>
+#
+# 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