Commit Diff


commit - 049da17d24a611282abd3553f6f43d75609a7fab
commit + 2ec1f75bbb4d6fb8f39613e5012392bae851aa8b
blob - 7bf08a3c2ec5f97189abca71a3b19ca7b24a3923
blob + 2944724c3dc3ffbb7c73c41c14ebf1f26a6d0d28
--- got/got.1
+++ got/got.1
@@ -286,7 +286,18 @@ Delete the reference with the specified name from the 
 .It Cm add Ar file-path
 Schedule an unversioned file in a work tree for addition to the
 repository in the next commit.
+.It Cm rm Ar file-path
+Remove a versioned file from a work tree and schedule it for deletion
+from the repository in the next commit.
+.Pp
+The options for
+.Cm got rm
+are as follows:
+.Bl -tag -width Ds
+.It Fl f
+Perform the operation even if the file contains uncommitted modifications.
 .El
+.El
 .Sh EXIT STATUS
 .Ex -std got
 .Sh EXAMPLES
blob - ec9e9c4a494ee6e79c2a52d3afaa7e13809f6a1d
blob + 9580988002d014eeb77fc0ac4b4948cf7d4ab414
--- got/got.c
+++ got/got.c
@@ -78,6 +78,7 @@ __dead static void	usage_tree(void);
 __dead static void	usage_status(void);
 __dead static void	usage_ref(void);
 __dead static void	usage_add(void);
+__dead static void	usage_rm(void);
 
 static const struct got_error*		cmd_checkout(int, char *[]);
 static const struct got_error*		cmd_update(int, char *[]);
@@ -88,6 +89,7 @@ static const struct got_error*		cmd_tree(int, char *[]
 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 struct cmd got_commands[] = {
 	{ "checkout",	cmd_checkout,	usage_checkout,
@@ -108,6 +110,8 @@ static struct cmd got_commands[] = {
 	    "manage references in repository" },
 	{ "add",	cmd_add,	usage_add,
 	    "add a new file to version control" },
+	{ "rm",		cmd_rm,		usage_rm,
+	    "remove a versioned file" },
 };
 
 int
@@ -996,7 +1000,8 @@ print_diff(void *arg, unsigned char status, const char
 	char *abspath = NULL;
 	struct stat sb;
 
-	if (status != GOT_STATUS_MODIFY && status != GOT_STATUS_ADD)
+	if (status != GOT_STATUS_MODIFY && status != GOT_STATUS_ADD &&
+	    status != GOT_STATUS_DELETE)
 		return NULL;
 
 	if (!a->header_shown) {
@@ -1005,28 +1010,31 @@ print_diff(void *arg, unsigned char status, const char
 		a->header_shown = 1;
 	}
 
-	if (status == GOT_STATUS_MODIFY) {
+	if (status == GOT_STATUS_MODIFY || status == GOT_STATUS_DELETE) {
 		err = got_object_open_as_blob(&blob1, a->repo, id, 8192);
 		if (err)
 			goto done;
 
 	}
 
-	if (asprintf(&abspath, "%s/%s",
-	    got_worktree_get_root_path(a->worktree), path) == -1) {
-		err = got_error_from_errno();
-		goto done;
-	}
+	if (status == GOT_STATUS_MODIFY || status == GOT_STATUS_ADD) {
+		if (asprintf(&abspath, "%s/%s",
+		    got_worktree_get_root_path(a->worktree), path) == -1) {
+			err = got_error_from_errno();
+			goto done;
+		}
 
-	f2 = fopen(abspath, "r");
-	if (f2 == NULL) {
-		err = got_error_from_errno();
-		goto done;
-	}
-	if (lstat(abspath, &sb) == -1) {
-		err = got_error_from_errno();
-		goto done;
-	}
+		f2 = fopen(abspath, "r");
+		if (f2 == NULL) {
+			err = got_error_from_errno();
+			goto done;
+		}
+		if (lstat(abspath, &sb) == -1) {
+			err = got_error_from_errno();
+			goto done;
+		}
+	} else
+		sb.st_size = 0;
 
 	err = got_diff_blob_file(blob1, f2, sb.st_size, path, a->diff_context,
 	    stdout);
@@ -1893,6 +1901,76 @@ done:
 		got_worktree_close(worktree);
 	free(path);
 	free(relpath);
+	free(cwd);
+	return error;
+}
+
+__dead static void
+usage_rm(void)
+{
+	fprintf(stderr, "usage: %s rm [-f] file-path\n", getprogname());
+	exit(1);
+}
+
+static const struct got_error *
+cmd_rm(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, delete_local_mods = 0;
+
+	while ((ch = getopt(argc, argv, "f")) != -1) {
+		switch (ch) {
+		case 'f':
+			delete_local_mods = 1;
+			break;
+		default:
+			usage_add();
+			/* NOTREACHED */
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc != 1)
+		usage_rm();
+
+	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(NULL, 0, got_worktree_get_root_path(worktree));
+	if (error)
+		goto done;
+
+	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;
 }
blob - 67326beff75d161b6b1590cd639b5563c4349e5a
blob + f60e3a768c9e7b2d3dc759b950409b01577ef15e
--- include/got_error.h
+++ include/got_error.h
@@ -80,6 +80,8 @@
 #define GOT_ERR_LOCKFILE_TIMEOUT 64
 #define GOT_ERR_BAD_REF_NAME	65
 #define GOT_ERR_WORKTREE_REPO	66
+#define GOT_ERR_FILE_MODIFIED	67
+#define GOT_ERR_FILE_STATUS	68
 
 static const struct got_error {
 	int code;
@@ -150,6 +152,8 @@ static const struct got_error {
 	{ GOT_ERR_LOCKFILE_TIMEOUT,"lockfile timeout" },
 	{ GOT_ERR_BAD_REF_NAME,	"bad reference name" },
 	{ GOT_ERR_WORKTREE_REPO,"cannot create worktree inside a git repository" },
+	{ GOT_ERR_FILE_MODIFIED,"file contains modifications" },
+	{ GOT_ERR_FILE_STATUS,	"file has unexpected status" },
 };
 
 /*
blob - 6c64d84a67bcf518f83f376fa4908c701fd84b6f
blob + 51e30c84ed559444e681b1e0d16454d4bf167cee
--- include/got_worktree.h
+++ include/got_worktree.h
@@ -139,3 +139,12 @@ const struct got_error *got_worktree_resolve_path(char
  */
 const struct got_error *got_worktree_schedule_add(char **,
     struct got_worktree *, const char *);
+
+/*
+ * Remove a file from disk and schedule it to be deleted in the next commit.
+ * Don't allow deleting files with uncommitted modifications, unless the
+ * parameter 'delete_local_mods' is set.
+ */
+const struct got_error *
+got_worktree_schedule_delete(struct got_worktree *, const char *, int,
+   got_worktree_status_cb, void *, struct got_repository *);
blob - 35307267948d55390cbdb4637114ef5fb2dae80d
blob + 3421739139d8ff2c8f0b5051debfd6d9755fd579
--- lib/fileindex.c
+++ lib/fileindex.c
@@ -42,6 +42,7 @@
 #define GOT_FILEIDX_F_NOT_FLUSHED	0x00010000
 #define GOT_FILEIDX_F_NO_BLOB		0x00020000
 #define GOT_FILEIDX_F_NO_COMMIT		0x00040000
+#define GOT_FILEIDX_F_NO_FILE_ON_DISK	0x00080000
 
 struct got_fileindex {
 	struct got_fileindex_tree entries;
@@ -59,6 +60,8 @@ got_fileindex_entry_update(struct got_fileindex_entry 
 	if (lstat(ondisk_path, &sb) != 0)
 		return got_error_from_errno();
 
+	entry->flags &= ~GOT_FILEIDX_F_NO_FILE_ON_DISK;
+
 	if (update_timestamps) {
 		entry->ctime_sec = sb.st_ctime;
 		entry->ctime_nsec = sb.st_ctimensec;
@@ -90,6 +93,12 @@ got_fileindex_entry_update(struct got_fileindex_entry 
 	return NULL;
 }
 
+void
+got_fileindex_entry_mark_deleted_from_disk(struct got_fileindex_entry *entry)
+{
+	entry->flags |= GOT_FILEIDX_F_NO_FILE_ON_DISK;
+}
+
 const struct got_error *
 got_fileindex_entry_alloc(struct got_fileindex_entry **entry,
     const char *ondisk_path, const char *relpath, uint8_t *blob_sha1,
@@ -137,6 +146,12 @@ got_fileindex_entry_has_commit(struct got_fileindex_en
 	return (ie->flags & GOT_FILEIDX_F_NO_COMMIT) == 0;
 }
 
+int
+got_fileindex_entry_has_file_on_disk(struct got_fileindex_entry *ie)
+{
+	return (ie->flags & GOT_FILEIDX_F_NO_FILE_ON_DISK) == 0;
+}
+
 static const struct got_error *
 add_entry(struct got_fileindex *fileindex, struct got_fileindex_entry *entry)
 {
blob - 98ab532f88824e32453c1ec38d506d903df29a8f
blob + 982945591293b88f08ae619ce9593f1eeb8bc22a
--- lib/got_lib_fileindex.h
+++ lib/got_lib_fileindex.h
@@ -141,3 +141,6 @@ const struct got_error *got_fileindex_diff_dir(struct 
 
 int got_fileindex_entry_has_blob(struct got_fileindex_entry *);
 int got_fileindex_entry_has_commit(struct got_fileindex_entry *);
+int got_fileindex_entry_has_file_on_disk(struct got_fileindex_entry *);
+
+void got_fileindex_entry_mark_deleted_from_disk(struct got_fileindex_entry *);
blob - bd919fbf217da75c8cea5d5941bca91798510925
blob + 9b86570baa1094b9fc829a3d2d4e98e1faf72521
--- lib/worktree.c
+++ lib/worktree.c
@@ -967,7 +967,10 @@ get_file_status(unsigned char *status, struct stat *sb
 	if (lstat(abspath, sb) == -1) {
 		if (errno == ENOENT) {
 			if (ie) {
-				*status = GOT_STATUS_MISSING;
+				if (got_fileindex_entry_has_file_on_disk(ie))
+					*status = GOT_STATUS_MISSING;
+				else
+					*status = GOT_STATUS_DELETE;
 				sb->st_mode =
 				    ((ie->mode >> GOT_FILEIDX_MODE_PERMS_SHIFT)
 				    & (S_IRWXU | S_IRWXG | S_IRWXO));
@@ -986,7 +989,10 @@ get_file_status(unsigned char *status, struct stat *sb
 	if (ie == NULL)
 		return NULL;
 
-	if (!got_fileindex_entry_has_blob(ie)) {
+	if (!got_fileindex_entry_has_file_on_disk(ie)) {
+		*status = GOT_STATUS_DELETE;
+		return NULL;
+	} else if (!got_fileindex_entry_has_blob(ie)) {
 		*status = GOT_STATUS_ADD;
 		return NULL;
 	}
@@ -1424,13 +1430,17 @@ status_old(void *arg, struct got_fileindex_entry *ie, 
 {
 	struct diff_dir_cb_arg *a = arg;
 	struct got_object_id id;
+	unsigned char status;
 
 	if (!got_path_is_child(parent_path, a->status_path, a->status_path_len))
 		return NULL;
 
 	memcpy(id.sha1, ie->blob_sha1, SHA1_DIGEST_LENGTH);
-	return (*a->status_cb)(a->status_arg, GOT_STATUS_MISSING, ie->path,
-	    &id);
+	if (got_fileindex_entry_has_file_on_disk(ie))
+		status = GOT_STATUS_MISSING;
+	else
+		status = GOT_STATUS_DELETE;
+	return (*a->status_cb)(a->status_arg, status, ie->path, &id);
 }
 
 static const struct got_error *
@@ -1511,7 +1521,7 @@ got_worktree_status(struct got_worktree *worktree, con
 	}
 	workdir = opendir(ondisk_path);
 	if (workdir == NULL) {
-		if (errno == ENOTDIR) {
+		if (errno == ENOTDIR || errno == ENOENT) {
 			struct got_fileindex_entry *ie;
 			ie = got_fileindex_entry_get(fileindex, path);
 			if (ie == NULL) {
@@ -1682,3 +1692,114 @@ done:
 	}
 	return err;
 }
+
+const struct got_error *
+got_worktree_schedule_delete(struct got_worktree *worktree,
+    const char *ondisk_path, int delete_local_mods,
+    got_worktree_status_cb status_cb, void *status_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;
+	FILE *index = NULL, *new_index = NULL;
+	const struct got_error *err = NULL, *unlockerr = 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;
+	}
+
+	err = get_file_status(&status, &sb, ie, ondisk_path, repo);
+	if (err)
+		goto done;
+
+	if (status != GOT_STATUS_NO_CHANGE) {
+		if (status != GOT_STATUS_MODIFY) {
+			err = got_error(GOT_ERR_FILE_STATUS);
+			goto done;
+		}
+		if (!delete_local_mods) {
+			err = got_error(GOT_ERR_FILE_MODIFIED);
+			goto done;
+		}
+	}
+
+	if (unlink(ondisk_path) != 0) {
+		err = got_error_from_errno();
+		goto done;
+	}
+
+	got_fileindex_entry_mark_deleted_from_disk(ie);
+
+	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;
+
+	err = report_file_status(ie, ondisk_path, status_cb, status_arg, repo);
+done:
+	free(relpath);
+	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 - fb63dcc0bfd3e3a0ceefa1d427f83a507225e6df
blob + 3692fa08e31570f8c6294c0cc7725dbadb4d573d
--- regress/cmdline/Makefile
+++ regress/cmdline/Makefile
@@ -1,4 +1,4 @@
-REGRESS_TARGETS=checkout update status log add
+REGRESS_TARGETS=checkout update status log add rm
 NOOBJ=Yes
 
 checkout:
@@ -16,4 +16,8 @@ log:
 add:
 	./add.sh
 
+rm:
+	./rm.sh
+
+
 .include <bsd.regress.mk>
blob - bdf87dc616a765027d46ff0a82ddf9c161e79ff8
blob + 84bbf2e8d42e2b4bfd8d1ad97db33cc0361a41d7
--- regress/cmdline/status.sh
+++ regress/cmdline/status.sh
@@ -27,6 +27,7 @@ function test_status_basic {
 	fi
 
 	echo "modified alpha" > $testroot/wt/alpha
+	(cd $testroot/wt && got rm beta >/dev/null)
 	echo "unversioned file" > $testroot/wt/foo
 	rm $testroot/wt/epsilon/zeta
 	touch $testroot/wt/beta
@@ -34,6 +35,7 @@ function test_status_basic {
 	(cd $testroot/wt && got add new >/dev/null)
 
 	echo 'M  alpha' > $testroot/stdout.expected
+	echo 'D  beta' >> $testroot/stdout.expected
 	echo '!  epsilon/zeta' >> $testroot/stdout.expected
 	echo '?  foo' >> $testroot/stdout.expected
 	echo 'A  new' >> $testroot/stdout.expected
blob - /dev/null
blob + 8753e23b6747409f7a3892319ef732fac8c6a45c (mode 755)
--- /dev/null
+++ regress/cmdline/rm.sh
@@ -0,0 +1,88 @@
+#!/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_rm_basic {
+	local testroot=`test_init rm_basic`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo 'D  beta' > $testroot/stdout.expected
+	(cd $testroot/wt && got rm beta > $testroot/stdout)
+
+	cmp $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+
+	if [ -e $testroot/wt/beta ]; then
+		echo "removed file beta still exists on disk" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+function test_rm_with_local_mods {
+	local testroot=`test_init rm_with_local_mods`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "modified beta" > $testroot/wt/beta
+	echo 'got: file contains modifications' > $testroot/stderr.expected
+	(cd $testroot/wt && got rm beta 2>$testroot/stderr)
+
+	cmp $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo 'D  beta' > $testroot/stdout.expected
+	(cd $testroot/wt && got rm -f beta > $testroot/stdout)
+
+	cmp $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+
+	if [ -e $testroot/wt/beta ]; then
+		echo "removed file beta still exists on disk" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+run_test test_rm_basic
+run_test test_rm_with_local_mods