"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Stefan Sperling <stsp@stsp.name>
Subject:
add 'got info' command
To:
gameoftrees@openbsd.org
Date:
Sat, 25 Jul 2020 20:10:10 +0200

Download raw body.

Thread
Got lacks a built-in command to examine work tree meta data.

It has always been possible to obtain some information from the files
in the .got/ directory because some of those files are plain-text:

 - Get the current branch:
   $ cat .got/head-ref

 - Get the current base commit:
   $ cat .got/base-commit

But viewing information stored in the file index is difficult. And making
this information more accessible could help with debugging problems with
a broken work tree.

This first implementation of 'got info' is not very flexible. I did not
add any options yet. The output can be parsed by scripts but my intention
is to add some options later to make scripting very easy.
Options which would make 'got info' print individual bits of information,
such as "show me the repository path for this work tree", and so on.
Similar functionality is offered by 'svn info --show-item' in Subversion.

My initial implementation just dumped everything by default but that can be
too much. The default output implemented in the patch below shows general
context information only:

$ got info
work tree: /usr/src
work tree base commit: 3ff74ee673f6439bbf658b9cb1b2ff65fbb3ee09
work tree path prefix: /
work tree branch reference: refs/heads/iwm-txagg
work tree UUID: bb7789ce-609a-472c-86e2-a42c1a58caff
repository: /git/src.git
$ 

Information stored in the file index can be appended to this general info
by specifying path arguments. (File index content would not be useful in
isolation, so general context info is always displayed.)

$ got info Makefile
work tree: /usr/src
work tree base commit: 3ff74ee673f6439bbf658b9cb1b2ff65fbb3ee09
work tree path prefix: /
work tree branch reference: refs/heads/iwm-txagg
work tree UUID: bb7789ce-609a-472c-86e2-a42c1a58caff
repository: /git/src.git
-----------------------------------------------
file: Makefile
mode: 644
timestamp: Mon Jul 13 14:53:54 2020 CEST
based on blob: 5002ac02445f9ab3b2fc89397a45f00b8b287fd2
based on commit: 20599cf97eccdb9ab18672df969fe10c00802e7d

Entire sub trees can also be shown:

$ got info sbin/ifconfig/
work tree: /usr/src
work tree base commit: 3ff74ee673f6439bbf658b9cb1b2ff65fbb3ee09
work tree path prefix: /
work tree branch reference: refs/heads/iwm-txagg
work tree UUID: bb7789ce-609a-472c-86e2-a42c1a58caff
repository: /git/src.git
-----------------------------------------------
file: sbin/ifconfig/Makefile
mode: 644
timestamp: Mon Jul 13 14:58:12 2020 CEST
based on blob: b674f82f00422b46f899c100a77d8ce875a4342b
based on commit: 20599cf97eccdb9ab18672df969fe10c00802e7d
-----------------------------------------------
file: sbin/ifconfig/brconfig.c
mode: 644
timestamp: Mon Jul 13 14:58:12 2020 CEST
based on blob: e33b73ebf2ca9d3cd26e08d63427e45e378c81af
based on commit: 20599cf97eccdb9ab18672df969fe10c00802e7d
-----------------------------------------------
file: sbin/ifconfig/ifconfig.8
mode: 644
timestamp: Mon Jul 13 14:58:12 2020 CEST
based on blob: 10111bf2c454181e92e103a0dd2bbfdbd77e9f90
based on commit: 20599cf97eccdb9ab18672df969fe10c00802e7d
-----------------------------------------------
file: sbin/ifconfig/ifconfig.c
mode: 644
timestamp: Mon Jul 13 14:58:12 2020 CEST
based on blob: cc314b7a44f8cd9a74ec6e32df6ce1cf8183a068
based on commit: 20599cf97eccdb9ab18672df969fe10c00802e7d
-----------------------------------------------
file: sbin/ifconfig/ifconfig.h
mode: 644
timestamp: Mon Jul 13 14:58:12 2020 CEST
based on blob: 1d5b9ccff10dc0a572b16fcff18d9de45bd00fef
based on commit: 20599cf97eccdb9ab18672df969fe10c00802e7d
-----------------------------------------------
file: sbin/ifconfig/sff.c
mode: 644
timestamp: Mon Jul 13 14:58:12 2020 CEST
based on blob: a800a1f55d0ee593deaaf7e091e22c4c4cc5e1b5
based on commit: 20599cf97eccdb9ab18672df969fe10c00802e7d
$

It is also possible to show all files, for example by specifying a dot
as the path argument: got info .
This is probably not intuitive and I did consider adding an option argument
for this purpose (such as -a, "show all"). But such an option becomes awkward
to handle in combination with path arguments so I decided to stick to just
path arguments for now. The man page explains how it works.

I am dropping the alias 'got in' (for 'got init').
This alias is too close to either 'init' or 'info'. Both commands are
short enough so I think the best approach is to remove this alias.

Unfortunately, the pledge for 'got info' is quite large:
	"stdio rpath wpath flock proc exec sendfd unveil"
The "proc exec sendfd" pledges are only needed because we are reading the
repository config file with 'got-read-gitconfig' when opening a work tree.
This could be improved later.

Any suggestions or concerns?

diff cf07f22bd8beb1ed9e048433d553e394cc323c5f /home/stsp/src/got
blob - 136fa542011eaf775871dd87e5f0c0a2ba5ef5a4
file + got/got.1
--- got/got.1
+++ got/got.1
@@ -73,9 +73,6 @@ the
 command must be used to populate the empty repository before
 .Cm got checkout
 can be used.
-.It Cm in
-Short alias for
-.Cm init .
 .It Cm import Oo Fl b Ar branch Oc Oo Fl m Ar message Oc Oo Fl r Ar repository-path Oc Oo Fl I Ar pattern Oc Ar directory
 Create an initial commit in a repository from the file hierarchy
 within the specified
@@ -1862,6 +1859,23 @@ Interpret all arguments as paths only.
 This option can be used to resolve ambiguity in cases where paths
 look like tag names, reference names, or object IDs.
 .El
+.It Cm info Op Ar path ...
+Display meta-data stored in a work tree.
+See
+.Xr got-worktree 5
+for details.
+.Pp
+The work tree to use is resolved implicitly by walking upwards from the
+current working directory.
+.Pp
+If one or more
+.Ar path
+arguments are specified, show additional per-file information for tracked
+files located at or within these paths.
+If a
+.Ar path
+argument corresponds to the work tree's root directory, display information
+for all tracked files.
 .El
 .Sh ENVIRONMENT
 .Bl -tag -width GOT_AUTHOR
@@ -1996,6 +2010,11 @@ working directory, so this command will log the subdir
 And this command has the same effect:
 .Pp
 .Dl $ cd sys/dev/usb && got log ../../uvm
+.Pp
+And this command displays work tree meta-data about all tracked files:
+.Pp
+.Dl $ cd /usr/src
+.Dl $ got info\ . | less
 .Pp
 Add new files and remove obsolete files in a work tree directory:
 .Pp
blob - fb2d55929f89f48f959e0ddfdc2fa639fe2c64d0
file + got/got.c
--- got/got.c
+++ got/got.c
@@ -109,6 +109,7 @@ __dead static void	usage_integrate(void);
 __dead static void	usage_stage(void);
 __dead static void	usage_unstage(void);
 __dead static void	usage_cat(void);
+__dead static void	usage_info(void);
 
 static const struct got_error*		cmd_init(int, char *[]);
 static const struct got_error*		cmd_import(int, char *[]);
@@ -136,9 +137,10 @@ static const struct got_error*		cmd_integrate(int, cha
 static const struct got_error*		cmd_stage(int, char *[]);
 static const struct got_error*		cmd_unstage(int, char *[]);
 static const struct got_error*		cmd_cat(int, char *[]);
+static const struct got_error*		cmd_info(int, char *[]);
 
 static struct got_cmd got_commands[] = {
-	{ "init",	cmd_init,	usage_init,	"in" },
+	{ "init",	cmd_init,	usage_init,	"" },
 	{ "import",	cmd_import,	usage_import,	"im" },
 	{ "clone",	cmd_clone,	usage_clone,	"cl" },
 	{ "fetch",	cmd_fetch,	usage_fetch,	"fe" },
@@ -164,6 +166,7 @@ static struct got_cmd got_commands[] = {
 	{ "stage",	cmd_stage,	usage_stage,	"sg" },
 	{ "unstage",	cmd_unstage,	usage_unstage,	"ug" },
 	{ "cat",	cmd_cat,	usage_cat,	"" },
+	{ "info",	cmd_info,	usage_info,	"" },
 };
 
 static void
@@ -9315,5 +9318,186 @@ done:
 		if (error == NULL)
 			error = repo_error;
 	}
+	return error;
+}
+
+__dead static void
+usage_info(void)
+{
+	fprintf(stderr, "usage: %s info [path ...]\n",
+	    getprogname());
+	exit(1);
+}
+
+static const struct got_error *
+print_path_info(void *arg, const char *path, mode_t mode, time_t mtime,
+    struct got_object_id *blob_id, struct got_object_id *staged_blob_id,
+    struct got_object_id *commit_id)
+{
+	const struct got_error *err = NULL;
+	char *id_str = NULL;
+	char datebuf[128];
+	struct tm mytm, *tm;
+	struct got_pathlist_head *paths = arg;
+	struct got_pathlist_entry *pe;
+
+	/*
+	 * Clear error indication from any of the path arguments which
+	 * would cause this file index entry to be displayed.
+	 */
+	TAILQ_FOREACH(pe, paths, entry) {
+		if (got_path_cmp(path, pe->path, strlen(path),
+		    pe->path_len) == 0 ||
+		    got_path_is_child(path, pe->path, pe->path_len))
+			pe->data = NULL; /* no error */
+	}
+
+	printf(GOT_COMMIT_SEP_STR);
+	if (S_ISLNK(mode))
+		printf("symlink: %s\n", path);
+	else if (S_ISREG(mode)) {
+		printf("file: %s\n", path);
+		printf("mode: %o\n", mode & (S_IRWXU | S_IRWXG | S_IRWXO));
+	} else if (S_ISDIR(mode))
+		printf("directory: %s\n", path);
+	else
+		printf("something: %s\n", path);
+
+	tm = localtime_r(&mtime, &mytm);
+	if (tm == NULL)
+		return NULL;
+	if (strftime(datebuf, sizeof(datebuf), "%c %Z", tm) >= sizeof(datebuf))
+		return got_error(GOT_ERR_NO_SPACE);
+	printf("timestamp: %s\n", datebuf);
+
+	if (blob_id) {
+		err = got_object_id_str(&id_str, blob_id);
+		if (err)
+			return err;
+		printf("based on blob: %s\n", id_str);
+		free(id_str);
+	}
+
+	if (staged_blob_id) {
+		err = got_object_id_str(&id_str, staged_blob_id);
+		if (err)
+			return err;
+		printf("based on staged blob: %s\n", id_str);
+		free(id_str);
+	}
+
+	if (commit_id) {
+		err = got_object_id_str(&id_str, commit_id);
+		if (err)
+			return err;
+		printf("based on commit: %s\n", id_str);
+		free(id_str);
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
+cmd_info(int argc, char *argv[])
+{
+	const struct got_error *error = NULL;
+	struct got_worktree *worktree = NULL;
+	char *cwd = NULL, *id_str = NULL;
+	struct got_pathlist_head paths;
+	struct got_pathlist_entry *pe;
+	char *uuidstr = NULL;
+	int ch, show_files = 0;
+
+	TAILQ_INIT(&paths);
+
+	while ((ch = getopt(argc, argv, "")) != -1) {
+		switch (ch) {
+		default:
+			usage_info();
+			/* NOTREACHED */
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+#ifndef PROFILE
+	if (pledge("stdio rpath wpath flock proc exec sendfd unveil",
+	    NULL) == -1)
+		err(1, "pledge");
+#endif
+	cwd = getcwd(NULL, 0);
+	if (cwd == NULL) {
+		error = got_error_from_errno("getcwd");
+		goto done;
+	}
+
+	error = got_worktree_open(&worktree, cwd);
+	if (error) {
+		if (error->code == GOT_ERR_NOT_WORKTREE)
+			error = wrap_not_worktree_error(error, "status", cwd);
+		goto done;
+	}
+
+	error = apply_unveil(NULL, 0, got_worktree_get_root_path(worktree));
+	if (error)
+		goto done;
+
+	if (argc >= 1) {
+		error = get_worktree_paths_from_argv(&paths, argc, argv,
+		    worktree);
+		if (error)
+			goto done;
+		show_files = 1;
+	}
+
+	error = got_object_id_str(&id_str,
+	    got_worktree_get_base_commit_id(worktree));
+	if (error)
+		goto done;
+
+	error = got_worktree_get_uuid(&uuidstr, worktree);
+	if (error)
+		goto done;
+
+	printf("work tree: %s\n", got_worktree_get_root_path(worktree));
+	printf("work tree base commit: %s\n", id_str);
+	printf("work tree path prefix: %s\n",
+	    got_worktree_get_path_prefix(worktree));
+	printf("work tree branch reference: %s\n",
+	    got_worktree_get_head_ref_name(worktree));
+	printf("work tree UUID: %s\n", uuidstr);
+	printf("repository: %s\n", got_worktree_get_repo_path(worktree));
+
+	if (show_files) {
+		struct got_pathlist_entry *pe;
+		TAILQ_FOREACH(pe, &paths, entry) {
+			if (pe->path_len == 0)
+				continue;
+			/*
+			 * Assume this path will fail. This will be corrected
+			 * in print_path_info() in case the path does suceeed.
+			 */
+			pe->data = (void *)got_error_path(pe->path,
+			    GOT_ERR_BAD_PATH);
+		}
+		error = got_worktree_path_info(worktree, &paths,
+		    print_path_info, &paths, check_cancelled, NULL);
+		if (error)
+			goto done;
+		TAILQ_FOREACH(pe, &paths, entry) {
+			if (pe->data != NULL) {
+				error = pe->data; /* bad path */
+				break;
+			}
+		}
+	}
+done:
+	TAILQ_FOREACH(pe, &paths, entry)
+		free((char *)pe->path);
+	got_pathlist_free(&paths);
+	free(cwd);
+	free(id_str);
+	free(uuidstr);
 	return error;
 }
blob - 277c66b0096dcee60d7ae5305877516328376f34
file + include/got_worktree.h
--- include/got_worktree.h
+++ include/got_worktree.h
@@ -75,6 +75,12 @@ const char *got_worktree_get_repo_path(struct got_work
 const char *got_worktree_get_path_prefix(struct got_worktree *);
 
 /*
+ * Get the UUID of a work tree as a string.
+ * The caller must dispose of the returned UUID string with free(3).
+ */
+const struct got_error *got_worktree_get_uuid(char **, struct got_worktree *);
+
+/*
  * Check if a user-provided path prefix matches that of the worktree.
  */
 const struct got_error *got_worktree_match_path_prefix(int *,
@@ -442,3 +448,18 @@ const struct got_error *got_worktree_stage(struct got_
 const struct got_error *got_worktree_unstage(struct got_worktree *,
     struct got_pathlist_head *, got_worktree_checkout_cb, void *,
     got_worktree_patch_cb, void *, struct got_repository *);
+
+/* A callback function which is invoked with per-path info. */
+typedef const struct got_error *(*got_worktree_path_info_cb)(void *,
+    const char *path, mode_t mode, time_t mtime,
+    struct got_object_id *blob_id, struct got_object_id *staged_blob_id,
+    struct got_object_id *commit_id);
+
+/* 
+ * Report the status of paths in the work tree.
+ * The info callback will be invoked with the provided void * argument,
+ * a path, and a corresponding status code.
+ */
+const struct got_error *
+got_worktree_path_info(struct got_worktree *, struct got_pathlist_head *,
+    got_worktree_path_info_cb, void *, got_cancel_cb , void *);
blob - 0a072932c20e3ee4dec70243cb7cdc3739d3c8ba
file + lib/worktree.c
--- lib/worktree.c
+++ lib/worktree.c
@@ -2143,21 +2143,33 @@ diff_new(void *arg, struct got_tree_entry *te, const c
 	return err;
 }
 
+const struct got_error *
+got_worktree_get_uuid(char **uuidstr, struct got_worktree *worktree)
+{
+	uint32_t uuid_status;
+
+	uuid_to_string(&worktree->uuid, uuidstr, &uuid_status);
+	if (uuid_status != uuid_s_ok) {
+		*uuidstr = NULL;
+		return got_error_uuid(uuid_status, "uuid_to_string");
+	}
+
+	return NULL;
+}
+
 static const struct got_error *
 get_ref_name(char **refname, struct got_worktree *worktree, const char *prefix)
 {
 	const struct got_error *err = NULL;
 	char *uuidstr = NULL;
-	uint32_t uuid_status;
 
 	*refname = NULL;
 
-	uuid_to_string(&worktree->uuid, &uuidstr, &uuid_status);
-	if (uuid_status != uuid_s_ok)
-		return got_error_uuid(uuid_status, "uuid_to_string");
+	err = got_worktree_get_uuid(&uuidstr, worktree);
+	if (err)
+		return err;
 
-	if (asprintf(refname, "%s-%s", prefix, uuidstr)
-	    == -1) {
+	if (asprintf(refname, "%s-%s", prefix, uuidstr) == -1) {
 		err = got_error_from_errno("asprintf");
 		*refname = NULL;
 	}
@@ -7667,6 +7679,95 @@ done:
 	if (fileindex)
 		got_fileindex_free(fileindex);
 	unlockerr = lock_worktree(worktree, LOCK_SH);
+	if (unlockerr && err == NULL)
+		err = unlockerr;
+	return err;
+}
+
+struct report_file_info_arg {
+	struct got_worktree *worktree;
+	got_worktree_path_info_cb info_cb;
+	void *info_arg;
+	struct got_pathlist_head *paths;
+	got_cancel_cb cancel_cb;
+	void *cancel_arg;
+};
+
+static const struct got_error *
+report_file_info(void *arg, struct got_fileindex_entry *ie)
+{
+	struct report_file_info_arg *a = arg;
+	struct got_pathlist_entry *pe;
+	struct got_object_id blob_id, staged_blob_id, commit_id;
+	struct got_object_id *blob_idp = NULL, *staged_blob_idp = NULL;
+	struct got_object_id *commit_idp = NULL;
+	int stage;
+
+	if (a->cancel_cb && a->cancel_cb(a->cancel_arg))
+		return got_error(GOT_ERR_CANCELLED);
+
+	TAILQ_FOREACH(pe, a->paths, entry) {
+		if (pe->path_len == 0 || strcmp(pe->path, ie->path) == 0 ||
+		    got_path_is_child(ie->path, pe->path, pe->path_len))
+			break;
+	}
+	if (pe == NULL) /* not found */
+		return NULL;
+
+	if (got_fileindex_entry_has_blob(ie)) {
+		memcpy(blob_id.sha1, ie->blob_sha1, SHA1_DIGEST_LENGTH);
+		blob_idp = &blob_id;
+	}
+	stage = got_fileindex_entry_stage_get(ie);
+	if (stage == GOT_FILEIDX_STAGE_MODIFY ||
+	    stage == GOT_FILEIDX_STAGE_ADD) {
+		memcpy(staged_blob_id.sha1, ie->staged_blob_sha1,
+		    SHA1_DIGEST_LENGTH);
+		staged_blob_idp = &staged_blob_id;
+	}
+
+	if (got_fileindex_entry_has_commit(ie)) {
+		memcpy(commit_id.sha1, ie->commit_sha1, SHA1_DIGEST_LENGTH);
+		commit_idp = &commit_id;
+	}
+
+	return a->info_cb(a->info_arg, ie->path, got_fileindex_perms_to_st(ie),
+	    (time_t)ie->mtime_sec, blob_idp, staged_blob_idp, commit_idp);
+}
+
+const struct got_error *
+got_worktree_path_info(struct got_worktree *worktree,
+    struct got_pathlist_head *paths,
+    got_worktree_path_info_cb info_cb, void *info_arg,
+    got_cancel_cb cancel_cb, void *cancel_arg)
+
+{
+	const struct got_error *err = NULL, *unlockerr;
+	struct got_fileindex *fileindex = NULL;
+	char *fileindex_path = NULL;
+	struct report_file_info_arg arg;
+
+	err = lock_worktree(worktree, LOCK_SH);
+	if (err)
+		return err;
+
+	err = open_fileindex(&fileindex, &fileindex_path, worktree);
+	if (err)
+		goto done;
+
+	arg.worktree = worktree;
+	arg.info_cb = info_cb;
+	arg.info_arg = info_arg;
+	arg.paths = paths;
+	arg.cancel_cb = cancel_cb;
+	arg.cancel_arg = cancel_arg;
+	err = got_fileindex_for_each_entry_safe(fileindex, report_file_info,
+	    &arg);
+done:
+	free(fileindex_path);
+	if (fileindex)
+		got_fileindex_free(fileindex);
+	unlockerr = lock_worktree(worktree, LOCK_UN);
 	if (unlockerr && err == NULL)
 		err = unlockerr;
 	return err;