Commit 5c1e53bc728ac0524d0471886bbc19f0b95e6c55

Stefan Sperling 2019-07-28T11:34:22

add support for multiple path arguments to 'got commit'

diff --git a/got/got.1 b/got/got.1
index b137227..035a4e6 100644
--- a/got/got.1
+++ b/got/got.1
@@ -476,12 +476,13 @@ it will be restored.
 .It Cm rv
 Short alias for
 .Cm revert .
-.It Cm commit [ Fl m Ar message ] [ path ]
+.It Cm commit [ Fl m Ar message ] [ path ... ]
 Create a new commit in the repository from local changes in a work tree
 and use this commit as the new base commit for the work tree.
-If a
+If no
 .Ar path
-is specified, only commit local changes at or within this path.
+is specified, commit all local changes in the work tree.
+Otherwise, commit local changes at or within the specified paths.
 .Pp
 Show the status of each affected file, using the following status codes:
 .Bl -column YXZ description
diff --git a/got/got.c b/got/got.c
index 2f130ca..41f97f7 100644
--- a/got/got.c
+++ b/got/got.c
@@ -3139,7 +3139,8 @@ done:
 __dead static void
 usage_commit(void)
 {
-	fprintf(stderr, "usage: %s commit [-m msg] [path]\n", getprogname());
+	fprintf(stderr, "usage: %s commit [-m msg] [path ...]\n",
+	    getprogname());
 	exit(1);
 }
 
@@ -3221,13 +3222,16 @@ cmd_commit(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, *id_str = NULL;
+	char *cwd = NULL, *id_str = NULL;
 	struct got_object_id *id = NULL;
 	const char *logmsg = NULL;
 	const char *got_author = getenv("GOT_AUTHOR");
 	struct collect_commit_logmsg_arg cl_arg;
 	char *editor = NULL;
 	int ch, rebase_in_progress;
+	struct got_pathlist_head paths;
+
+	TAILQ_INIT(&paths);
 
 	while ((ch = getopt(argc, argv, "m:")) != -1) {
 		switch (ch) {
@@ -3248,16 +3252,6 @@ cmd_commit(int argc, char *argv[])
 	    "unveil", NULL) == -1)
 		err(1, "pledge");
 #endif
-	if (argc == 1) {
-		path = realpath(argv[0], NULL);
-		if (path == NULL) {
-			error = got_error_from_errno2("realpath", argv[0]);
-			goto done;
-		}
-		got_path_strip_trailing_slashes(path);
-	} else if (argc != 0)
-		usage_commit();
-
 	if (got_author == NULL) {
 		/* TODO: Look current user up in password database */
 		error = got_error(GOT_ERR_COMMIT_NO_AUTHOR);
@@ -3281,6 +3275,10 @@ cmd_commit(int argc, char *argv[])
 		goto done;
 	}
 
+	error = get_worktree_paths_from_argv(&paths, argc, argv, worktree);
+	if (error)
+		goto done;
+
 	error = got_repo_open(&repo, got_worktree_get_repo_path(worktree));
 	if (error != NULL)
 		goto done;
@@ -3307,7 +3305,7 @@ cmd_commit(int argc, char *argv[])
 		cl_arg.branch_name += 6;
 	cl_arg.repo_path = got_repo_get_path(repo);
 	cl_arg.logmsg_path = NULL;
-	error = got_worktree_commit(&id, worktree, path, got_author, NULL,
+	error = got_worktree_commit(&id, worktree, &paths, got_author, NULL,
 	    collect_commit_logmsg, &cl_arg, print_status, NULL, repo);
 	if (error) {
 		if (cl_arg.logmsg_path)
@@ -3328,7 +3326,6 @@ done:
 		got_repo_close(repo);
 	if (worktree)
 		got_worktree_close(worktree);
-	free(path);
 	free(cwd);
 	free(id_str);
 	free(editor);
diff --git a/include/got_worktree.h b/include/got_worktree.h
index 105bebd..f4f431a 100644
--- a/include/got_worktree.h
+++ b/include/got_worktree.h
@@ -199,11 +199,10 @@ typedef const struct got_error *(*got_worktree_commit_msg_cb)(
  * current base commit.
  * An author and a non-empty log message must be specified.
  * The name of the committer is optional (may be NULL).
- * If an on-disk path is specified, only commit changes at or within this path.
  */
 const struct got_error *got_worktree_commit(struct got_object_id **,
-    struct got_worktree *, const char *, const char *, const char *,
-    got_worktree_commit_msg_cb, void *,
+    struct got_worktree *, struct got_pathlist_head *, const char *,
+    const char *, got_worktree_commit_msg_cb, void *,
     got_worktree_status_cb, void *, struct got_repository *);
 
 /* Get the path of a commitable worktree item. */
diff --git a/lib/worktree.c b/lib/worktree.c
index 1103157..77022f3 100644
--- a/lib/worktree.c
+++ b/lib/worktree.c
@@ -3340,7 +3340,7 @@ const struct got_error *
 commit_worktree(struct got_object_id **new_commit_id,
     struct got_pathlist_head *commitable_paths,
     struct got_object_id *head_commit_id, struct got_worktree *worktree,
-    const char *ondisk_path, const char *author, const char *committer,
+    const char *author, const char *committer,
     got_worktree_commit_msg_cb commit_msg_cb, void *commit_arg,
     got_worktree_status_cb status_cb, void *status_arg,
     struct got_repository *repo)
@@ -3465,9 +3465,34 @@ done:
 	return err;
 }
 
+static const struct got_error *
+check_path_is_commitable(const char *path,
+    struct got_pathlist_head *commitable_paths)
+{
+	struct got_pathlist_entry *cpe = NULL;
+	size_t path_len = strlen(path);
+
+	TAILQ_FOREACH(cpe, commitable_paths, entry) {
+		struct got_commitable *ct = cpe->data;
+		const char *ct_path = ct->path;
+
+		while (ct_path[0] == '/')
+			ct_path++;
+
+		if (strcmp(path, ct_path) == 0 ||
+		    got_path_is_child(ct_path, path, path_len))
+			break;
+	}
+
+	if (cpe == NULL)
+		return got_error_path(path, GOT_ERR_BAD_PATH);
+
+	return NULL;
+}
+
 const struct got_error *
 got_worktree_commit(struct got_object_id **new_commit_id,
-    struct got_worktree *worktree, const char *ondisk_path,
+    struct got_worktree *worktree, struct got_pathlist_head *paths,
     const char *author, const char *committer,
     got_worktree_commit_msg_cb commit_msg_cb, void *commit_arg,
     got_worktree_status_cb status_cb, void *status_arg,
@@ -3475,7 +3500,7 @@ got_worktree_commit(struct got_object_id **new_commit_id,
 {
 	const struct got_error *err = NULL, *unlockerr = NULL, *sync_err;
 	struct got_fileindex *fileindex = NULL;
-	char *fileindex_path = NULL, *relpath = NULL;
+	char *fileindex_path = NULL;
 	struct got_pathlist_head commitable_paths;
 	struct collect_commitables_arg cc_arg;
 	struct got_pathlist_entry *pe;
@@ -3498,21 +3523,6 @@ got_worktree_commit(struct got_object_id **new_commit_id,
 	if (err)
 		goto done;
 
-	if (ondisk_path) {
-		if (strcmp(ondisk_path, worktree->root_path) == 0) {
-			relpath = strdup("");
-			if (relpath == NULL) {
-				err = got_error_from_errno("strdup");
-				goto done;
-			}
-		} else {
-			err = got_path_skip_common_ancestor(&relpath,
-			    worktree->root_path, ondisk_path);
-			if (err)
-				return err;
-		}
-	}
-
 	err = open_fileindex(&fileindex, &fileindex_path, worktree);
 	if (err)
 		goto done;
@@ -3520,16 +3530,24 @@ got_worktree_commit(struct got_object_id **new_commit_id,
 	cc_arg.commitable_paths = &commitable_paths;
 	cc_arg.worktree = worktree;
 	cc_arg.repo = repo;
-	err = worktree_status(worktree, relpath ? relpath : "",
-	    fileindex, repo, collect_commitables, &cc_arg, NULL, NULL);
-	if (err)
-		goto done;
+	TAILQ_FOREACH(pe, paths, entry) {
+		err = worktree_status(worktree, pe->path, fileindex, repo,
+		    collect_commitables, &cc_arg, NULL, NULL);
+		if (err)
+			goto done;
+	}
 
 	if (TAILQ_EMPTY(&commitable_paths)) {
 		err = got_error(GOT_ERR_COMMIT_NO_CHANGES);
 		goto done;
 	}
 
+	TAILQ_FOREACH(pe, paths, entry) {
+		err = check_path_is_commitable(pe->path, &commitable_paths);
+		if (err)
+			goto done;
+	}
+
 	TAILQ_FOREACH(pe, &commitable_paths, entry) {
 		struct got_commitable *ct = pe->data;
 		err = check_ct_out_of_date(ct, repo, head_commit_id);
@@ -3538,7 +3556,7 @@ got_worktree_commit(struct got_object_id **new_commit_id,
 	}
 
 	err = commit_worktree(new_commit_id, &commitable_paths,
-	    head_commit_id, worktree, ondisk_path, author, committer,
+	    head_commit_id, worktree, author, committer,
 	    commit_msg_cb, commit_arg, status_cb, status_arg, repo);
 	if (err)
 		goto done;
@@ -3552,7 +3570,6 @@ done:
 	if (fileindex)
 		got_fileindex_free(fileindex);
 	free(fileindex_path);
-	free(relpath);
 	unlockerr = lock_worktree(worktree, LOCK_SH);
 	if (unlockerr && err == NULL)
 		err = unlockerr;
@@ -4088,7 +4105,7 @@ rebase_commit(struct got_object_id **new_commit_id,
 		return got_error_from_errno("strdup");
 
 	err = commit_worktree(new_commit_id, &commitable_paths, head_commit_id,
-	    worktree, NULL, got_object_commit_get_author(orig_commit),
+	    worktree, got_object_commit_get_author(orig_commit),
 	    got_object_commit_get_committer(orig_commit),
 	    collect_rebase_commit_msg, logmsg, rebase_status, NULL, repo);
 	if (err)
diff --git a/regress/cmdline/commit.sh b/regress/cmdline/commit.sh
index a8ec799..579f23d 100755
--- a/regress/cmdline/commit.sh
+++ b/regress/cmdline/commit.sh
@@ -419,6 +419,58 @@ function test_commit_dir_path {
 	test_done "$testroot" "$ret"
 }
 
+function test_commit_selected_paths {
+	local testroot=`test_init commit_selected_paths`
+
+	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 "modified delta" > $testroot/wt/gamma/delta
+	echo "modified zeta" > $testroot/wt/epsilon/zeta
+	(cd $testroot/wt && got rm beta >/dev/null)
+	echo "new file" > $testroot/wt/new
+	(cd $testroot/wt && got add new >/dev/null)
+
+	(cd $testroot/wt && got commit -m 'many paths' nonexistent alpha \
+		> $testroot/stdout 2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "commit succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+	echo "got: nonexistent: bad path" > $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
+
+	(cd $testroot/wt && got commit -m 'many paths' \
+		beta new gamma > $testroot/stdout)
+
+	local head_rev=`git_show_head $testroot/repo`
+	echo "A  new" > $testroot/stdout.expected
+	echo "D  beta" >> $testroot/stdout.expected
+	echo "M  gamma/delta" >> $testroot/stdout.expected
+	echo "Created commit $head_rev" >> $testroot/stdout.expected
+
+	cmp -s $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_commit_basic
 run_test test_commit_new_subdir
 run_test test_commit_subdir
@@ -430,3 +482,4 @@ run_test test_commit_single_file_multiple
 run_test test_commit_added_and_modified_in_same_dir
 run_test test_commit_path_prefix
 run_test test_commit_dir_path
+run_test test_commit_selected_paths