Commit 17ed46186c2a79ae984817ddcdab9f803f23636a

Stefan Sperling 2019-06-02T21:24:17

allow removing multiple paths at once for 'got rm'

diff --git a/TODO b/TODO
index 8f54d73..aca7b79 100644
--- a/TODO
+++ b/TODO
@@ -1,7 +1,6 @@
 lib:
 - handle checkout of trees which contain submodules by identifying and
   ignoring such tree entries; requires a .ini config parser (from isakmpd?)
-- allow removing multiple paths at once for 'got rm'
 - allow adding directory paths with 'got add'
 - recursive addition: got add -R
 - recursive removal: got rm -R
diff --git a/got/got.1 b/got/got.1
index 524d8ee..4790d3d 100644
--- a/got/got.1
+++ b/got/got.1
@@ -310,8 +310,8 @@ Delete the reference with the specified name from the repository.
 .It Cm add Ar file-path ...
 Schedule unversioned files 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
+.It Cm rm Ar file-path ...
+Remove versioned files from a work tree and schedule them for deletion
 from the repository in the next commit.
 .Pp
 The options for
@@ -319,7 +319,7 @@ The options for
 are as follows:
 .Bl -tag -width Ds
 .It Fl f
-Perform the operation even if the file contains uncommitted modifications.
+Perform the operation even if a file contains uncommitted modifications.
 .El
 .It Cm revert Ar file-path
 Revert any uncommited changes in the file at the specified path.
diff --git a/got/got.c b/got/got.c
index fd6712a..5b82d9b 100644
--- a/got/got.c
+++ b/got/got.c
@@ -2215,8 +2215,12 @@ 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;
+	char *cwd = NULL;
+	struct got_pathlist_head paths;
+	struct got_pathlist_entry *pe;
+	int ch, i, delete_local_mods = 0;
+
+	TAILQ_INIT(&paths);
 
 	while ((ch = getopt(argc, argv, "f")) != -1) {
 		switch (ch) {
@@ -2232,15 +2236,18 @@ cmd_rm(int argc, char *argv[])
 	argc -= optind;
 	argv += optind;
 
-	if (argc != 1)
+	if (argc < 1)
 		usage_rm();
 
-	path = realpath(argv[0], NULL);
-	if (path == NULL) {
-		error = got_error_from_errno2("realpath", argv[0]);
-		goto done;
+	/* make sure each file exists before doing anything halfway */
+	for (i = 0; i < argc; i++) {
+		char *path = realpath(argv[i], NULL);
+		if (path == NULL) {
+			error = got_error_from_errno2("realpath", argv[i]);
+			goto done;
+		}
+		free(path);
 	}
-	got_path_strip_trailing_slashes(path);
 
 	cwd = getcwd(NULL, 0);
 	if (cwd == NULL) {
@@ -2260,8 +2267,22 @@ cmd_rm(int argc, char *argv[])
 	if (error)
 		goto done;
 
-	error = got_worktree_schedule_delete(worktree, path, delete_local_mods,
-	    print_status, NULL, repo);
+	for (i = 0; i < argc; i++) {
+		char *path = realpath(argv[i], NULL);
+		if (path == NULL) {
+			error = got_error_from_errno2("realpath", argv[i]);
+			goto done;
+		}
+
+		got_path_strip_trailing_slashes(path);
+		error = got_pathlist_insert(&pe, &paths, path, NULL);
+		if (error) {
+			free(path);
+			goto done;
+		}
+	}
+	error = got_worktree_schedule_delete(worktree, &paths,
+	    delete_local_mods, print_status, NULL, repo);
 	if (error)
 		goto done;
 done:
@@ -2269,7 +2290,9 @@ done:
 		got_repo_close(repo);
 	if (worktree)
 		got_worktree_close(worktree);
-	free(path);
+	TAILQ_FOREACH(pe, &paths, entry)
+		free((char *)pe->path);
+	got_pathlist_free(&paths);
 	free(cwd);
 	return error;
 }
diff --git a/include/got_worktree.h b/include/got_worktree.h
index 5dbd069..9189973 100644
--- a/include/got_worktree.h
+++ b/include/got_worktree.h
@@ -160,13 +160,14 @@ const struct got_error *got_worktree_schedule_add(struct got_worktree *,
     struct got_repository *);
 
 /*
- * Remove a file from disk and schedule it to be deleted in the next commit.
+ * Remove files from disk and schedule them 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 *);
+got_worktree_schedule_delete(struct got_worktree *,
+    struct got_pathlist_head *, int, got_worktree_status_cb, void *,
+    struct got_repository *);
 
 /*
  * Revert a file at the specified path such that it matches its
diff --git a/lib/worktree.c b/lib/worktree.c
index ca87d48..39f7d97 100644
--- a/lib/worktree.c
+++ b/lib/worktree.c
@@ -2263,29 +2263,57 @@ done:
 	return err;
 }
 
+static const struct got_error *
+schedule_for_deletion(const char *ondisk_path, struct got_fileindex *fileindex,
+    const char *relpath, int delete_local_mods,
+    got_worktree_status_cb status_cb, void *status_arg,
+    struct got_repository *repo)
+{
+	const struct got_error *err = NULL;
+	struct got_fileindex_entry *ie = NULL;
+	unsigned char status;
+	struct stat sb;
+
+	ie = got_fileindex_entry_get(fileindex, relpath);
+	if (ie == NULL)
+		return got_error(GOT_ERR_BAD_PATH);
+
+	err = get_file_status(&status, &sb, ie, ondisk_path, repo);
+	if (err)
+		return err;
+
+	if (status != GOT_STATUS_NO_CHANGE) {
+		if (status == GOT_STATUS_DELETE)
+			return got_error_set_errno(ENOENT, ondisk_path);
+		if (status != GOT_STATUS_MODIFY)
+			return got_error(GOT_ERR_FILE_STATUS);
+		if (!delete_local_mods)
+			return got_error(GOT_ERR_FILE_MODIFIED);
+	}
+
+	if (unlink(ondisk_path) != 0)
+		return got_error_from_errno2("unlink", ondisk_path);
+
+	got_fileindex_entry_mark_deleted_from_disk(ie);
+	return report_file_status(ie, ondisk_path, status_cb, status_arg, repo);
+}
+
 const struct got_error *
 got_worktree_schedule_delete(struct got_worktree *worktree,
-    const char *ondisk_path, int delete_local_mods,
+    struct got_pathlist_head *ondisk_paths, 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 ;
+	char *fileindex_path = NULL;
 	FILE *index = NULL;
 	const struct got_error *err = NULL, *unlockerr = NULL;
-	unsigned char status;
-	struct stat sb;
+	struct got_pathlist_entry *pe;
 
 	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("got_fileindex_alloc");
@@ -2309,45 +2337,23 @@ got_worktree_schedule_delete(struct got_worktree *worktree,
 	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_DELETE) {
-			err = got_error_set_errno(ENOENT, ondisk_path);
-			goto done;
-		}
-		if (status != GOT_STATUS_MODIFY) {
-			err = got_error(GOT_ERR_FILE_STATUS);
+	TAILQ_FOREACH(pe, ondisk_paths, entry) {
+		char *relpath;
+		err = got_path_skip_common_ancestor(&relpath,
+		    got_worktree_get_root_path(worktree), pe->path);
+		if (err)
 			goto done;
-		}
-		if (!delete_local_mods) {
-			err = got_error(GOT_ERR_FILE_MODIFIED);
+		err = schedule_for_deletion(pe->path, fileindex, relpath,
+		    delete_local_mods, status_cb, status_arg, repo);
+		free(relpath);
+		if (err)
 			goto done;
-		}
 	}
 
-	if (unlink(ondisk_path) != 0) {
-		err = got_error_from_errno2("unlink", ondisk_path);
-		goto done;
-	}
-
-	got_fileindex_entry_mark_deleted_from_disk(ie);
-
 	err = sync_fileindex(fileindex, fileindex_path);
 	if (err)
 		goto done;
-
-	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("fclose");
diff --git a/regress/cmdline/rm.sh b/regress/cmdline/rm.sh
index bb717ca..34059af 100755
--- a/regress/cmdline/rm.sh
+++ b/regress/cmdline/rm.sh
@@ -26,22 +26,37 @@ function test_rm_basic {
 		return 1
 	fi
 
-	echo 'D  beta' > $testroot/stdout.expected
-	(cd $testroot/wt && got rm beta > $testroot/stdout)
+	echo 'D  alpha' > $testroot/stdout.expected
+	echo 'D  beta' >> $testroot/stdout.expected
+	(cd $testroot/wt && got rm alpha beta > $testroot/stdout)
 
 	cmp -s $testroot/stdout.expected $testroot/stdout
 	ret="$?"
 	if [ "$ret" != "0" ]; then
 		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
 	fi
 
-	if [ -e $testroot/wt/beta ]; then
-		echo "removed file beta still exists on disk" >&2
-		test_done "$testroot" "1"
+	(cd $testroot/wt && got status > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
 		return 1
 	fi
 
-	test_done "$testroot" "$ret"
+	for f in alpha beta; do
+		if [ -e $testroot/wt/$f ]; then
+			echo "removed file $f still exists on disk" >&2
+			test_done "$testroot" "1"
+			return 1
+		fi
+	done
+
+	test_done "$testroot" "0"
 }
 
 function test_rm_with_local_mods {