Commit Diff


commit - dbb83fbd81591d01db580caf15e585de893f0b9b
commit + f2a9dc41d851ff2d575b08c2766583ff11cdd7af
blob - 6b4ec9a6c5c0f9240144d60d19c72d8af64f0b0e
blob + de86180518a585614d01189c9b485d5f8fa8573c
--- TODO
+++ TODO
@@ -11,7 +11,6 @@ lib:
 
 got:
 - 'histedit -c' prompts for log message even if there are no changes to commit
-- recursive removal: got rm -R
 
 tog:
 - implement horizonal scrolling in all views
blob - fa001c2db61fe9127ca82232eb555991984c9dcb
blob + 42403d111ef601d3428f3750208becb5665fcfc0
--- got/got.1
+++ got/got.1
@@ -645,7 +645,7 @@ With -R, add files even if they match a
 .Cm got status
 ignore pattern.
 .El
-.It Cm remove Ar file-path ...
+.It Cm remove Oo Fl R Oc Ar file-path ...
 Remove versioned files from a work tree and schedule them for deletion
 from the repository in the next commit.
 .Pp
@@ -655,6 +655,13 @@ are as follows:
 .Bl -tag -width Ds
 .It Fl f
 Perform the operation even if a file contains uncommitted modifications.
+.It Fl R
+Permit recursion into directories.
+If this option is not specified,
+.Cm got remove
+will refuse to run if a specified
+.Ar path
+is a directory.
 .El
 .It Cm rm
 Short alias for
blob - 67836e9dac99a1e361945334537edee3a628ccf6
blob + 5b73c98b079e00ed10051699743ef328c40800d9
--- got/got.c
+++ got/got.c
@@ -4276,16 +4276,17 @@ done:
 __dead static void
 usage_remove(void)
 {
-	fprintf(stderr, "usage: %s remove [-f] file-path ...\n", getprogname());
+	fprintf(stderr, "usage: %s remove [-f] [-R] file-path ...\n",
+	    getprogname());
 	exit(1);
 }
 
 static const struct got_error *
 print_remove_status(void *arg, unsigned char status,
-    unsigned char staged_status, const char *path,
-    struct got_object_id *blob_id, struct got_object_id *staged_blob_id,
-    struct got_object_id *commit_id)
+    unsigned char staged_status, const char *path)
 {
+	while (path[0] == '/')
+		path++;
 	if (status == GOT_STATUS_NONEXISTENT)
 		return NULL;
 	if (status == staged_status && (status == GOT_STATUS_DELETE))
@@ -4303,17 +4304,20 @@ cmd_remove(int argc, char *argv[])
 	char *cwd = NULL;
 	struct got_pathlist_head paths;
 	struct got_pathlist_entry *pe;
-	int ch, delete_local_mods = 0;
+	int ch, delete_local_mods = 0, can_recurse = 0;
 
 	TAILQ_INIT(&paths);
 
-	while ((ch = getopt(argc, argv, "f")) != -1) {
+	while ((ch = getopt(argc, argv, "fR")) != -1) {
 		switch (ch) {
 		case 'f':
 			delete_local_mods = 1;
 			break;
+		case 'R':
+			can_recurse = 1;
+			break;
 		default:
-			usage_add();
+			usage_remove();
 			/* NOTREACHED */
 		}
 	}
@@ -4352,6 +4356,35 @@ cmd_remove(int argc, char *argv[])
 	if (error)
 		goto done;
 
+	if (!can_recurse) {
+		char *ondisk_path;
+		struct stat sb;
+		TAILQ_FOREACH(pe, &paths, entry) {
+			if (asprintf(&ondisk_path, "%s/%s",
+			    got_worktree_get_root_path(worktree),
+			       pe->path) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+			if (lstat(ondisk_path, &sb) == -1) {
+				if (errno == ENOENT) {
+					free(ondisk_path);
+					continue;
+				}
+				error = got_error_from_errno2("lstat",
+				    ondisk_path);
+				free(ondisk_path);
+				goto done;
+			}
+			free(ondisk_path);
+			if (S_ISDIR(sb.st_mode)) {
+				error = got_error_msg(GOT_ERR_BAD_PATH,
+				    "removing directories requires -R option");
+				goto done;
+			}
+		}
+	}
+
 	error = got_worktree_schedule_delete(worktree, &paths,
 	    delete_local_mods, print_remove_status, NULL, repo);
 	if (error)
blob - 33c5e5f8529ba9bf1fcf626666817f9317db7d02
blob + 3aaddd7755f9fd3f50ff866dbf1c0774053f89d3
--- include/got_worktree.h
+++ include/got_worktree.h
@@ -103,6 +103,10 @@ const struct got_error *got_worktree_set_base_commit_i
 /* A callback function which is invoked when a path is checked out. */
 typedef const struct got_error *(*got_worktree_checkout_cb)(void *,
     unsigned char, const char *);
+
+/* A callback function which is invoked when a path is removed. */
+typedef const struct got_error *(*got_worktree_delete_cb)(void *,
+    unsigned char, unsigned char, const char *);
 
 /*
  * Attempt to check out files into a work tree from its associated repository
@@ -168,7 +172,7 @@ const struct got_error *got_worktree_schedule_add(stru
  */
 const struct got_error *
 got_worktree_schedule_delete(struct got_worktree *,
-    struct got_pathlist_head *, int, got_worktree_status_cb, void *,
+    struct got_pathlist_head *, int, got_worktree_delete_cb, void *,
     struct got_repository *);
 
 /* A callback function which is used to select or reject a patch. */
blob - 96c1281dca23c97dc546bd56d59ab67efd907667
blob + fa31d1e75f6e61a0d42966b946ed7caccc110980
--- lib/worktree.c
+++ lib/worktree.c
@@ -2308,12 +2308,13 @@ struct diff_dir_cb_arg {
     void *cancel_arg;
     /* A pathlist containing per-directory pathlists of ignore patterns. */
     struct got_pathlist_head ignores;
+    int report_unchanged;
 };
 
 static const struct got_error *
 report_file_status(struct got_fileindex_entry *ie, const char *abspath,
     got_worktree_status_cb status_cb, void *status_arg,
-    struct got_repository *repo)
+    struct got_repository *repo, int report_unchanged)
 {
 	const struct got_error *err = NULL;
 	unsigned char status = GOT_STATUS_NO_CHANGE;
@@ -2328,7 +2329,7 @@ report_file_status(struct got_fileindex_entry *ie, con
 		return err;
 
 	if (status == GOT_STATUS_NO_CHANGE &&
-	    staged_status == GOT_STATUS_NO_CHANGE)
+	    staged_status == GOT_STATUS_NO_CHANGE && !report_unchanged)
 		return NULL;
 
 	if (got_fileindex_entry_has_blob(ie)) {
@@ -2377,7 +2378,7 @@ status_old_new(void *arg, struct got_fileindex_entry *
 	}
 
 	err = report_file_status(ie, abspath, a->status_cb, a->status_arg,
-	    a->repo);
+	    a->repo, a->report_unchanged);
 	free(abspath);
 	return err;
 }
@@ -2608,7 +2609,7 @@ status_new(void *arg, struct dirent *de, const char *p
 static const struct got_error *
 report_single_file_status(const char *path, const char *ondisk_path,
 struct got_fileindex *fileindex, got_worktree_status_cb status_cb,
-void *status_arg, struct got_repository *repo)
+void *status_arg, struct got_repository *repo, int report_unchanged)
 {
 	struct got_fileindex_entry *ie;
 	struct stat sb;
@@ -2616,7 +2617,7 @@ void *status_arg, struct got_repository *repo)
 	ie = got_fileindex_entry_get(fileindex, path, strlen(path));
 	if (ie)
 		return report_file_status(ie, ondisk_path, status_cb,
-		    status_arg, repo);
+		    status_arg, repo, report_unchanged);
 
 	if (lstat(ondisk_path, &sb) == -1) {
 		if (errno != ENOENT)
@@ -2637,7 +2638,8 @@ static const struct got_error *
 worktree_status(struct got_worktree *worktree, const char *path,
     struct got_fileindex *fileindex, struct got_repository *repo,
     got_worktree_status_cb status_cb, void *status_arg,
-    got_cancel_cb cancel_cb, void *cancel_arg, int no_ignores)
+    got_cancel_cb cancel_cb, void *cancel_arg, int no_ignores,
+    int report_unchanged)
 {
 	const struct got_error *err = NULL;
 	DIR *workdir = NULL;
@@ -2655,7 +2657,8 @@ worktree_status(struct got_worktree *worktree, const c
 			err = got_error_from_errno2("opendir", ondisk_path);
 		else
 			err = report_single_file_status(path, ondisk_path,
-			    fileindex, status_cb, status_arg, repo);
+			    fileindex, status_cb, status_arg, repo,
+			    report_unchanged);
 	} else {
 		fdiff_cb.diff_old_new = status_old_new;
 		fdiff_cb.diff_old = status_old;
@@ -2669,6 +2672,7 @@ worktree_status(struct got_worktree *worktree, const c
 		arg.status_arg = status_arg;
 		arg.cancel_cb = cancel_cb;
 		arg.cancel_arg = cancel_arg;
+		arg.report_unchanged = report_unchanged;
 		TAILQ_INIT(&arg.ignores);
 		if (!no_ignores) {
 			err = add_ignores(&arg.ignores, worktree->root_path,
@@ -2706,7 +2710,7 @@ got_worktree_status(struct got_worktree *worktree,
 
 	TAILQ_FOREACH(pe, paths, entry) {
 		err = worktree_status(worktree, pe->path, fileindex, repo,
-			status_cb, status_arg, cancel_cb, cancel_arg, 0);
+			status_cb, status_arg, cancel_cb, cancel_arg, 0, 0);
 		if (err)
 			break;
 	}
@@ -2861,7 +2865,7 @@ got_worktree_schedule_add(struct got_worktree *worktre
 
 	TAILQ_FOREACH(pe, paths, entry) {
 		err = worktree_status(worktree, pe->path, fileindex, repo,
-			schedule_addition, &saa, NULL, NULL, no_ignores);
+			schedule_addition, &saa, NULL, NULL, no_ignores, 0);
 		if (err)
 			break;
 	}
@@ -2878,18 +2882,28 @@ 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)
+struct schedule_deletion_args {
+	struct got_worktree *worktree;
+	struct got_fileindex *fileindex;
+	got_worktree_delete_cb progress_cb;
+	void *progress_arg;
+	struct got_repository *repo;
+	int delete_local_mods;
+};
+
+static const struct got_error *
+schedule_for_deletion(void *arg, unsigned char status,
+    unsigned char staged_status, const char *relpath,
+    struct got_object_id *blob_id, struct got_object_id *staged_blob_id,
+    struct got_object_id *commit_id)
 {
+	struct schedule_deletion_args *a = arg;
 	const struct got_error *err = NULL;
 	struct got_fileindex_entry *ie = NULL;
-	unsigned char status, staged_status;
 	struct stat sb;
+	char *ondisk_path;
 
-	ie = got_fileindex_entry_get(fileindex, relpath, strlen(relpath));
+	ie = got_fileindex_entry_get(a->fileindex, relpath, strlen(relpath));
 	if (ie == NULL)
 		return got_error(GOT_ERR_BAD_PATH);
 
@@ -2900,37 +2914,55 @@ schedule_for_deletion(const char *ondisk_path, struct 
 		return got_error_path(relpath, GOT_ERR_FILE_STAGED);
 	}
 
-	err = get_file_status(&status, &sb, ie, ondisk_path, repo);
+	if (asprintf(&ondisk_path, "%s/%s", a->worktree->root_path,
+	    relpath) == -1)
+		return got_error_from_errno("asprintf");
+
+	err = get_file_status(&status, &sb, ie, ondisk_path, a->repo);
 	if (err)
-		return err;
+		goto done;
 
 	if (status != GOT_STATUS_NO_CHANGE) {
 		if (status == GOT_STATUS_DELETE)
-			return NULL;
-		if (status == GOT_STATUS_MODIFY && !delete_local_mods)
-			return got_error_path(relpath, GOT_ERR_FILE_MODIFIED);
-		if (status != GOT_STATUS_MODIFY &&
-		    status != GOT_STATUS_MISSING)
-			return got_error_path(relpath, GOT_ERR_FILE_STATUS);
+			goto done;
+		if (status == GOT_STATUS_MODIFY && !a->delete_local_mods) {
+			err = got_error_path(relpath, GOT_ERR_FILE_MODIFIED);
+			goto done;
+		}
+		if (status != GOT_STATUS_MODIFY &&
+		    status != GOT_STATUS_MISSING) {
+			err = got_error_path(relpath, GOT_ERR_FILE_STATUS);
+			goto done;
+		}
 	}
 
-	if (status != GOT_STATUS_MISSING && unlink(ondisk_path) != 0)
-		return got_error_from_errno2("unlink", ondisk_path);
+	if (status != GOT_STATUS_MISSING && unlink(ondisk_path) != 0) {
+		err = got_error_from_errno2("unlink", ondisk_path);
+		goto done;
+	}
 
 	got_fileindex_entry_mark_deleted_from_disk(ie);
-	return report_file_status(ie, ondisk_path, status_cb, status_arg, repo);
+done:
+	free(ondisk_path);
+	if (err)
+		return err;
+	if (status == GOT_STATUS_DELETE)
+		return NULL;
+	return (*a->progress_cb)(a->progress_arg, GOT_STATUS_DELETE,
+	    staged_status, relpath);
 }
 
 const struct got_error *
 got_worktree_schedule_delete(struct got_worktree *worktree,
     struct got_pathlist_head *paths, int delete_local_mods,
-    got_worktree_status_cb status_cb, void *status_arg,
+    got_worktree_delete_cb progress_cb, void *progress_arg,
     struct got_repository *repo)
 {
 	struct got_fileindex *fileindex = NULL;
 	char *fileindex_path = NULL;
 	const struct got_error *err = NULL, *sync_err, *unlockerr;
 	struct got_pathlist_entry *pe;
+	struct schedule_deletion_args sda;
 
 	err = lock_worktree(worktree, LOCK_EX);
 	if (err)
@@ -2939,15 +2971,17 @@ got_worktree_schedule_delete(struct got_worktree *work
 	err = open_fileindex(&fileindex, &fileindex_path, worktree);
 	if (err)
 		goto done;
+
+	sda.worktree = worktree;
+	sda.fileindex = fileindex;
+	sda.progress_cb = progress_cb;
+	sda.progress_arg = progress_arg;
+	sda.repo = repo;
+	sda.delete_local_mods = delete_local_mods;
 
 	TAILQ_FOREACH(pe, paths, entry) {
-		char *ondisk_path;
-		if (asprintf(&ondisk_path, "%s/%s", worktree->root_path,
-		    pe->path) == -1)
-			return got_error_from_errno("asprintf");
-		err = schedule_for_deletion(ondisk_path, fileindex, pe->path,
-		    delete_local_mods, status_cb, status_arg, repo);
-		free(ondisk_path);
+		err = worktree_status(worktree, pe->path, fileindex, repo,
+			schedule_for_deletion, &sda, NULL, NULL, 0, 1);
 		if (err)
 			break;
 	}
@@ -3508,7 +3542,7 @@ got_worktree_revert(struct got_worktree *worktree,
 	rfa.repo = repo;
 	TAILQ_FOREACH(pe, paths, entry) {
 		err = worktree_status(worktree, pe->path, fileindex, repo,
-		    revert_file, &rfa, NULL, NULL, 0);
+		    revert_file, &rfa, NULL, NULL, 0, 0);
 		if (err)
 			break;
 	}
@@ -4421,7 +4455,7 @@ got_worktree_commit(struct got_object_id **new_commit_
 	cc_arg.have_staged_files = have_staged_files;
 	TAILQ_FOREACH(pe, paths, entry) {
 		err = worktree_status(worktree, pe->path, fileindex, repo,
-		    collect_commitables, &cc_arg, NULL, NULL, 0);
+		    collect_commitables, &cc_arg, NULL, NULL, 0, 0);
 		if (err)
 			goto done;
 	}
@@ -4979,13 +5013,14 @@ rebase_commit(struct got_object_id **new_commit_id,
 		}
 		TAILQ_FOREACH(pe, merged_paths, entry) {
 			err = worktree_status(worktree, pe->path, fileindex,
-			    repo, collect_commitables, &cc_arg, NULL, NULL, 0);
+			    repo, collect_commitables, &cc_arg, NULL, NULL, 0,
+			    0);
 			if (err)
 				goto done;
 		}
 	} else {
 		err = worktree_status(worktree, "", fileindex, repo,
-		    collect_commitables, &cc_arg, NULL, NULL, 0);
+		    collect_commitables, &cc_arg, NULL, NULL, 0, 0);
 		if (err)
 			goto done;
 	}
@@ -5293,7 +5328,7 @@ got_worktree_rebase_abort(struct got_worktree *worktre
 	rfa.patch_arg = NULL;
 	rfa.repo = repo;
 	err = worktree_status(worktree, "", fileindex, repo,
-	    revert_file, &rfa, NULL, NULL, 0);
+	    revert_file, &rfa, NULL, NULL, 0, 0);
 	if (err)
 		goto sync;
 
@@ -5646,7 +5681,7 @@ got_worktree_histedit_abort(struct got_worktree *workt
 	rfa.patch_arg = NULL;
 	rfa.repo = repo;
 	err = worktree_status(worktree, "", fileindex, repo,
-	    revert_file, &rfa, NULL, NULL, 0);
+	    revert_file, &rfa, NULL, NULL, 0, 0);
 	if (err)
 		goto sync;
 
@@ -6102,7 +6137,7 @@ got_worktree_stage(struct got_worktree *worktree,
 	oka.have_changes = 0;
 	TAILQ_FOREACH(pe, paths, entry) {
 		err = worktree_status(worktree, pe->path, fileindex, repo,
-		    check_stage_ok, &oka, NULL, NULL, 0);
+		    check_stage_ok, &oka, NULL, NULL, 0, 0);
 		if (err)
 			goto done;
 	}
@@ -6121,7 +6156,7 @@ got_worktree_stage(struct got_worktree *worktree,
 	spa.staged_something = 0;
 	TAILQ_FOREACH(pe, paths, entry) {
 		err = worktree_status(worktree, pe->path, fileindex, repo,
-		    stage_path, &spa, NULL, NULL, 0);
+		    stage_path, &spa, NULL, NULL, 0, 0);
 		if (err)
 			goto done;
 	}
@@ -6472,7 +6507,7 @@ got_worktree_unstage(struct got_worktree *worktree,
 	upa.patch_arg = patch_arg;
 	TAILQ_FOREACH(pe, paths, entry) {
 		err = worktree_status(worktree, pe->path, fileindex, repo,
-		    unstage_path, &upa, NULL, NULL, 0);
+		    unstage_path, &upa, NULL, NULL, 0, 0);
 		if (err)
 			goto done;
 	}
blob - 03c8d8fc147c4298c5425973fe32318e931e8992
blob + 551feb1fb11346676d2f161b062a605bde93e6d9
--- regress/cmdline/rm.sh
+++ regress/cmdline/rm.sh
@@ -193,7 +193,57 @@ function test_rm_and_add_elsewhere {
 	test_done "$testroot" "$ret"
 }
 
+function test_rm_directory {
+	local testroot=`test_init rm_directory`
+
+	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 . > $testroot/stdout 2> $testroot/stderr)
+	ret="$?"
+	echo "got: removing directories requires -R option" \
+		> $testroot/stderr.expected
+	cmp -s $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 -n > $testroot/stdout.expected
+	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
+
+	(cd $testroot/wt && got rm -R . > $testroot/stdout)
+
+	echo 'D  alpha' > $testroot/stdout.expected
+	echo 'D  beta' >> $testroot/stdout.expected
+	echo 'D  epsilon/zeta' >> $testroot/stdout.expected
+	echo 'D  gamma/delta' >> $testroot/stdout.expected
+
+	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"
+}
+
 run_test test_rm_basic
 run_test test_rm_with_local_mods
 run_test test_double_rm
 run_test test_rm_and_add_elsewhere
+run_test test_rm_directory