Commit c42961445798bbd0ae6062fee1914b918ecca593

Stefan Sperling 2019-05-07T10:17:36

WIP commit implementation

diff --git a/got/Makefile b/got/Makefile
index a0d4587..a11f5ad 100644
--- a/got/Makefile
+++ b/got/Makefile
@@ -5,7 +5,8 @@ SRCS=		got.c blame.c commit_graph.c delta.c diff.c diffoffset.c \
 		diffreg.c error.c fileindex.c object.c object_cache.c \
 		object_idset.c object_parse.c opentemp.c path.c pack.c \
 		privsep.c reference.c repository.c sha1.c worktree.c \
-		inflate.c buf.c worklist.c rcsutil.c diff3.c lockfile.c
+		inflate.c buf.c worklist.c rcsutil.c diff3.c lockfile.c \
+		deflate.c object_create.c
 
 CPPFLAGS = -I${.CURDIR}/../include -I${.CURDIR}/../lib \
 	-DGOT_LIBEXECDIR=${GOT_LIBEXECDIR}
diff --git a/got/got.c b/got/got.c
index 3ae9806..f48a524 100644
--- a/got/got.c
+++ b/got/got.c
@@ -80,6 +80,7 @@ __dead static void	usage_ref(void);
 __dead static void	usage_add(void);
 __dead static void	usage_rm(void);
 __dead static void	usage_revert(void);
+__dead static void	usage_commit(void);
 
 static const struct got_error*		cmd_checkout(int, char *[]);
 static const struct got_error*		cmd_update(int, char *[]);
@@ -92,6 +93,7 @@ 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 const struct got_error*		cmd_commit(int, char *[]);
 
 static struct cmd got_commands[] = {
 	{ "checkout",	cmd_checkout,	usage_checkout,
@@ -116,6 +118,8 @@ static struct cmd got_commands[] = {
 	    "remove a versioned file" },
 	{ "revert",	cmd_revert,	usage_revert,
 	    "revert uncommitted changes" },
+	{ "commit",	cmd_commit,	usage_commit,
+	    "create blob from file (WIP; can't create commits yet)" },
 };
 
 int
@@ -2067,3 +2071,82 @@ done:
 	free(cwd);
 	return error;
 }
+
+__dead static void
+usage_commit(void)
+{
+	fprintf(stderr, "usage: %s commit file-path\n", getprogname());
+	exit(1);
+}
+
+static const struct got_error *
+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;
+	struct got_object_id *id = NULL;
+	const char *logmsg = "<no log message was specified>";
+	int ch;
+
+	while ((ch = getopt(argc, argv, "m:")) != -1) {
+		switch (ch) {
+		case 'm':
+			logmsg = optarg;
+			break;
+		default:
+			usage_commit();
+			/* NOTREACHED */
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc == 1) {
+		path = realpath(argv[0], NULL);
+		if (path == NULL) {
+			error = got_error_from_errno();
+			goto done;
+		}
+	} else if (argc != 0)
+		usage_commit();
+
+
+	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), 0,
+	    got_worktree_get_root_path(worktree));
+	if (error)
+		goto done;
+
+	error = got_worktree_commit(&id, worktree, path, logmsg, repo);
+	if (error)
+		goto done;
+
+	error = got_object_id_str(&id_str, id);
+	if (error)
+		goto done;
+	printf("created commit %s\n", id_str);
+done:
+	if (repo)
+		got_repo_close(repo);
+	if (worktree)
+		got_worktree_close(worktree);
+	free(path);
+	free(cwd);
+	free(id_str);
+	return error;
+}
diff --git a/include/got_error.h b/include/got_error.h
index 9a844fe..c5fb394 100644
--- a/include/got_error.h
+++ b/include/got_error.h
@@ -82,6 +82,7 @@
 #define GOT_ERR_WORKTREE_REPO	66
 #define GOT_ERR_FILE_MODIFIED	67
 #define GOT_ERR_FILE_STATUS	68
+#define GOT_ERR_COMMIT_CONFLICT	69
 
 static const struct got_error {
 	int code;
@@ -155,6 +156,7 @@ static const struct got_error {
 	{ 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" },
+	{ GOT_ERR_COMMIT_CONFLICT,"cannot commit file in conflicted status" },
 };
 
 /*
diff --git a/include/got_worktree.h b/include/got_worktree.h
index b3cc035..debcb30 100644
--- a/include/got_worktree.h
+++ b/include/got_worktree.h
@@ -164,3 +164,17 @@ got_worktree_schedule_delete(struct got_worktree *, const char *, int,
  */
 const struct got_error *got_worktree_revert(struct got_worktree *,
     const char *, got_worktree_checkout_cb, void *, struct got_repository *);
+
+/*
+ * Create a new commit from changes in the work tree.
+ * Return the ID of the newly created commit.
+ * The worktree's base commit will be set to this new commit.
+ * Files unaffected by this commit operation will retain their
+ * current base commit.
+ * A non-empty log message must be specified.
+ * 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 *,
+    struct got_repository *);
diff --git a/lib/worktree.c b/lib/worktree.c
index 92720c5..e8812fd 100644
--- a/lib/worktree.c
+++ b/lib/worktree.c
@@ -2196,3 +2196,96 @@ done:
 		err = unlockerr;
 	return err;
 }
+
+static const struct got_error *
+collect_committables(void *arg, unsigned char status, const char *path,
+    struct got_object_id *id)
+{
+	struct got_pathlist_head *paths = arg;
+	const struct got_error *err = NULL;
+	char *new_path = NULL;
+	unsigned char *new_status = NULL;
+	struct got_pathlist_entry *new = NULL;
+
+	if (status == GOT_STATUS_CONFLICT)
+		return got_error(GOT_ERR_COMMIT_CONFLICT);
+
+	if (status != GOT_STATUS_MODIFY && status != GOT_STATUS_ADD &&
+	    status != GOT_STATUS_DELETE)
+		return NULL;
+	
+	new_path = strdup(path);
+	if (new_path == NULL)
+		return got_error_from_errno();
+
+	new_status = malloc(sizeof(*new_status));
+	if (new_status == NULL) {
+		err = got_error_from_errno();
+		goto done;
+	}
+
+	*new_status = status;
+	err = got_pathlist_insert(&new, paths, new_path, new_status);
+done:
+	if (err || new == NULL) {
+		free(new_path);
+		free(new_status);
+	}
+	return err;
+}
+
+const struct got_error *
+got_worktree_commit(struct got_object_id **new_commit_id,
+    struct got_worktree *worktree, const char *ondisk_path,
+    const char *logmsg, struct got_repository *repo)
+{
+	const struct got_error *err = NULL, *unlockerr = NULL;
+	struct got_pathlist_head paths;
+	struct got_pathlist_entry *pe;
+	char *relpath = NULL;
+	struct got_commit_object *base_commit = NULL;
+	struct got_tree_object *base_tree = NULL;
+
+	*new_commit_id = NULL;
+
+	TAILQ_INIT(&paths);
+
+	if (ondisk_path) {
+		err = got_path_skip_common_ancestor(&relpath,
+		    worktree->root_path, ondisk_path);
+		if (err)
+			return err;
+	}
+
+	err = lock_worktree(worktree, LOCK_EX);
+	if (err)
+		goto done;
+
+	err = got_object_open_as_commit(&base_commit, repo,
+	    worktree->base_commit_id);
+	if (err)
+		goto done;
+	err = got_object_open_as_tree(&base_tree, repo,
+	    worktree->base_commit_id);
+	if (err)
+		goto done;
+
+	err = got_worktree_status(worktree, relpath ? relpath : "",
+	    repo, collect_committables, &paths, NULL, NULL);
+	if (err)
+		goto done;
+
+	/* TODO: walk base tree and patch it to create a new tree */
+	printf("committables:\n");
+	TAILQ_FOREACH(pe, &paths, entry) {
+		unsigned char *status = pe->data;
+		printf("%c %s\n", *status, pe->path);
+	}
+done:
+	unlockerr = lock_worktree(worktree, LOCK_SH);
+	if (unlockerr && err == NULL)
+		err = unlockerr;
+	got_object_tree_close(base_tree);
+	free(relpath);
+	return err;
+}
diff --git a/regress/cmdline/commit.sh b/regress/cmdline/commit.sh
new file mode 100755
index 0000000..0a36cee
--- /dev/null
+++ b/regress/cmdline/commit.sh
@@ -0,0 +1,53 @@
+#!/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_commit_basic {
+	local testroot=`test_init commit_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
+	(cd $testroot/wt && got rm beta >/dev/null)
+	echo "unversioned file" > $testroot/wt/foo
+	rm $testroot/wt/epsilon/zeta
+	touch $testroot/wt/beta
+	echo "new file" > $testroot/wt/new
+	(cd $testroot/wt && got add new >/dev/null)
+
+	(cd $testroot/wt && got commit -m 'test commit_basic' > $testroot/stdout)
+
+	local head_rev=`git_show_head $testroot/repo`
+	echo "M alpha" > $testroot/stdout.expected
+	echo "D beta" >> $testroot/stdout.expected
+	echo "A new" >> $testroot/stdout.expected
+	echo "created commit $head_rev" >> $testroot/stdout.expected
+
+	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_commit_basic