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

From:
Tracey Emery <tracey@traceyemery.net>
Subject:
Re: got import -> gotadmin import
To:
gameoftrees@openbsd.org
Date:
Wed, 6 Jul 2022 08:05:04 -0600

Download raw body.

Thread
On Wed, Jul 06, 2022 at 08:58:44AM +0200, Stefan Sperling wrote:
> This patch moves 'got import' into gotadmin.
> 
> Having written the patch I am not quite sure if this is really
> a good idea. The stated purpose of gotadmin is "inspecting and
> manipulating the on-disk state of Git repositories". I am not
> sure if creating new root commits (vendor branches) should be
> part of this scope. Perhaps 'got import' is indeed the better
> place for this functionality?
> 
> We would also have to move some code from got.c into lib/ so it could
> be shared with gotadmin, in particular for the creation of log messages.
> This creates a lot of code churn.
> 
> Does anyone very much prefer gotadmin import? Or should I drop the patch?

I prefer both got init and got import, but have no strong opinion. It
feels weird to me moving those to gotadmin. There are cases where I
would never have a server-side component for several repos, if I
remember to got init move argument.

> 
> diff 230e1f1bfa1d7bbd0dee0217e22bc2817e848ad0 3866985167c3ce83469b78e6553bf975cab3abbb
> commit - 230e1f1bfa1d7bbd0dee0217e22bc2817e848ad0
> commit + 3866985167c3ce83469b78e6553bf975cab3abbb
> blob - 1b45b53a4efff9977dcd3c2e2e33c499adc94533
> blob + 9b8612199aff6a53330920d1cac9ef819e02703e
> --- got/Makefile
> +++ got/Makefile
> @@ -13,7 +13,8 @@ SRCS=		got.c blame.c commit_graph.c delta.c diff.c \
>  		diff_myers.c diff_output.c diff_output_plain.c \
>  		diff_output_unidiff.c diff_output_edscript.c \
>  		diff_patience.c send.c deltify.c pack_create.c dial.c \
> -		bloom.c murmurhash2.c ratelimit.c patch.c sigs.c date.c
> +		bloom.c murmurhash2.c ratelimit.c patch.c sigs.c date.c \
> +		logmsg.c
>  
>  MAN =		${PROG}.1 got-worktree.5 git-repository.5 got.conf.5
>  
> blob - 5c511f97ce933f13c1d706a8c3cc7ce271ca6cb5
> blob + 549e764508b315e8fc0e6c338daa2ae76e2d9f82
> --- got/got.1
> +++ got/got.1
> @@ -62,67 +62,6 @@ The commands for
>  .Nm
>  are as follows:
>  .Bl -tag -width checkout
> -.Tg im
> -.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
> -.Dl Pq alias: Cm im
> -Create an initial commit in a repository from the file hierarchy
> -within the specified
> -.Ar directory .
> -The created commit will not have any parent commits, i.e. it will be a
> -root commit.
> -Also create a new reference which provides a branch name for the newly
> -created commit.
> -Show the path of each imported file to indicate progress.
> -.Pp
> -The
> -.Cm got import
> -command requires the
> -.Ev GOT_AUTHOR
> -environment variable to be set,
> -unless an author has been configured in
> -.Xr got.conf 5
> -or Git's
> -.Dv user.name
> -and
> -.Dv user.email
> -configuration settings can be obtained from the repository's
> -.Pa .git/config
> -file or from Git's global
> -.Pa ~/.gitconfig
> -configuration file.
> -.Pp
> -The options for
> -.Cm got import
> -are as follows:
> -.Bl -tag -width Ds
> -.It Fl b Ar branch
> -Create the specified
> -.Ar branch
> -instead of creating the default branch
> -.Dq main .
> -Use of this option is required if the
> -.Dq main
> -branch already exists.
> -.It Fl m Ar message
> -Use the specified log message when creating the new commit.
> -Without the
> -.Fl m
> -option,
> -.Cm got import
> -opens a temporary file in an editor where a log message can be written.
> -.It Fl r Ar repository-path
> -Use the repository at the specified path.
> -If not specified, assume the repository is located at or above the current
> -working directory.
> -.It Fl I Ar pattern
> -Ignore files or directories with a name which matches the specified
> -.Ar pattern .
> -This option may be specified multiple times to build a list of ignore patterns.
> -The
> -.Ar pattern
> -follows the globbing rules documented in
> -.Xr glob 7 .
> -.El
>  .Tg cl
>  .It Cm clone Oo Fl a Oc Oo Fl b Ar branch Oc Oo Fl l Oc Oo Fl m Oc Oo Fl q Oc Oo Fl v Oc Oo Fl R Ar reference Oc Ar repository-URL Op Ar directory
>  .Dl Pq alias: Cm cl
> @@ -2354,7 +2293,7 @@ should be preferred over
>  However, even strictly linear projects may require merge commits in order
>  to merge in new versions of third-party code stored on vendor branches
>  created with
> -.Cm got import .
> +.Cm gotadmin import .
>  .Pp
>  Merge commits are commits based on multiple parent commits.
>  The tip commit of the work tree's current branch, which must be set with
> @@ -2699,7 +2638,7 @@ for all tracked files.
>  The author's name and email address for
>  .Cm got commit
>  and
> -.Cm got import ,
> +.Cm gotadmin import ,
>  for example:
>  .Dq An Flan Hacker Aq Mt flan_hacker@openbsd.org .
>  Because
> @@ -2734,7 +2673,7 @@ environment variable provide author information.
>  The editor spawned by
>  .Cm got commit ,
>  .Cm got histedit ,
> -.Cm got import ,
> +.Cm gotadmin import ,
>  or
>  .Cm got tag .
>  If not set, the
> @@ -2803,7 +2742,7 @@ e.g. from a temporary CVS checkout located at
>  .Pa /tmp/src :
>  .Pp
>  .Dl $ gotadmin init /var/git/src.git
> -.Dl $ got import -r /var/git/src.git -I CVS -I obj /tmp/src
> +.Dl $ gotadmin import -r /var/git/src.git -I CVS -I obj /tmp/src
>  .Pp
>  Check out a work tree from the Git repository to /usr/src:
>  .Pp
> blob - 77ee3290c2b299ba86499c5b196115d3f54e96b6
> blob + 80f9c5e10dd970222405707e8ead1d7bfe4aa971
> --- got/got.c
> +++ got/got.c
> @@ -61,6 +61,7 @@
>  #include "got_patch.h"
>  #include "got_sigs.h"
>  #include "got_date.h"
> +#include "got_logmsg.h"
>  
>  #ifndef nitems
>  #define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
> @@ -90,7 +91,6 @@ struct got_cmd {
>  };
>  
>  __dead static void	usage(int, int);
> -__dead static void	usage_import(void);
>  __dead static void	usage_clone(void);
>  __dead static void	usage_fetch(void);
>  __dead static void	usage_checkout(void);
> @@ -120,7 +120,6 @@ __dead static void	usage_unstage(void);
>  __dead static void	usage_cat(void);
>  __dead static void	usage_info(void);
>  
> -static const struct got_error*		cmd_import(int, char *[]);
>  static const struct got_error*		cmd_clone(int, char *[]);
>  static const struct got_error*		cmd_fetch(int, char *[]);
>  static const struct got_error*		cmd_checkout(int, char *[]);
> @@ -151,7 +150,6 @@ static const struct got_error*		cmd_cat(int, char *[])
>  static const struct got_error*		cmd_info(int, char *[]);
>  
>  static const struct got_cmd got_commands[] = {
> -	{ "import",	cmd_import,	usage_import,	"im" },
>  	{ "clone",	cmd_clone,	usage_clone,	"cl" },
>  	{ "fetch",	cmd_fetch,	usage_fetch,	"fe" },
>  	{ "checkout",	cmd_checkout,	usage_checkout,	"co" },
> @@ -289,33 +287,6 @@ usage(int hflag, int status)
>  }
>  
>  static const struct got_error *
> -get_editor(char **abspath)
> -{
> -	const struct got_error *err = NULL;
> -	const char *editor;
> -
> -	*abspath = NULL;
> -
> -	editor = getenv("VISUAL");
> -	if (editor == NULL)
> -		editor = getenv("EDITOR");
> -
> -	if (editor) {
> -		err = got_path_find_prog(abspath, editor);
> -		if (err)
> -			return err;
> -	}
> -
> -	if (*abspath == NULL) {
> -		*abspath = strdup("/bin/ed");
> -		if (*abspath == NULL)
> -			return got_error_from_errno("strdup");
> -	}
> -
> -	return NULL;
> -}
> -
> -static const struct got_error *
>  apply_unveil(const char *repo_path, int repo_read_only,
>      const char *worktree_path)
>  {
> @@ -344,296 +315,7 @@ apply_unveil(const char *repo_path, int repo_read_only
>  	return NULL;
>  }
>  
> -__dead static void
> -usage_import(void)
> -{
> -	fprintf(stderr, "usage: %s import [-b branch] [-m message] "
> -	    "[-r repository-path] [-I pattern] path\n", getprogname());
> -	exit(1);
> -}
> -
> -static int
> -spawn_editor(const char *editor, const char *file)
> -{
> -	pid_t pid;
> -	sig_t sighup, sigint, sigquit;
> -	int st = -1;
> -
> -	sighup = signal(SIGHUP, SIG_IGN);
> -	sigint = signal(SIGINT, SIG_IGN);
> -	sigquit = signal(SIGQUIT, SIG_IGN);
> -
> -	switch (pid = fork()) {
> -	case -1:
> -		goto doneediting;
> -	case 0:
> -		execl(editor, editor, file, (char *)NULL);
> -		_exit(127);
> -	}
> -
> -	while (waitpid(pid, &st, 0) == -1)
> -		if (errno != EINTR)
> -			break;
> -
> -doneediting:
> -	(void)signal(SIGHUP, sighup);
> -	(void)signal(SIGINT, sigint);
> -	(void)signal(SIGQUIT, sigquit);
> -
> -	if (!WIFEXITED(st)) {
> -		errno = EINTR;
> -		return -1;
> -	}
> -
> -	return WEXITSTATUS(st);
> -}
> -
>  static const struct got_error *
> -edit_logmsg(char **logmsg, const char *editor, const char *logmsg_path,
> -    const char *initial_content, size_t initial_content_len,
> -    int require_modification)
> -{
> -	const struct got_error *err = NULL;
> -	char *line = NULL;
> -	size_t linesize = 0;
> -	ssize_t linelen;
> -	struct stat st, st2;
> -	FILE *fp = NULL;
> -	size_t len, logmsg_len;
> -	char *initial_content_stripped = NULL, *buf = NULL, *s;
> -
> -	*logmsg = NULL;
> -
> -	if (stat(logmsg_path, &st) == -1)
> -		return got_error_from_errno2("stat", logmsg_path);
> -
> -	if (spawn_editor(editor, logmsg_path) == -1)
> -		return got_error_from_errno("failed spawning editor");
> -
> -	if (stat(logmsg_path, &st2) == -1)
> -		return got_error_from_errno("stat");
> -
> -	if (require_modification &&
> -	    st.st_mtime == st2.st_mtime && st.st_size == st2.st_size)
> -		return got_error_msg(GOT_ERR_COMMIT_MSG_EMPTY,
> -		    "no changes made to commit message, aborting");
> -
> -	/*
> -	 * Set up a stripped version of the initial content without comments
> -	 * and blank lines. We need this in order to check if the message
> -	 * has in fact been edited.
> -	 */
> -	initial_content_stripped = malloc(initial_content_len + 1);
> -	if (initial_content_stripped == NULL)
> -		return got_error_from_errno("malloc");
> -	initial_content_stripped[0] = '\0';
> -
> -	buf = strdup(initial_content);
> -	if (buf == NULL) {
> -		err = got_error_from_errno("strdup");
> -		goto done;
> -	}
> -	s = buf;
> -	len = 0;
> -	while ((line = strsep(&s, "\n")) != NULL) {
> -		if ((line[0] == '#' || (len == 0 && line[0] == '\n')))
> -			continue; /* remove comments and leading empty lines */
> -		len = strlcat(initial_content_stripped, line,
> -		    initial_content_len + 1);
> -		if (len >= initial_content_len + 1) {
> -			err = got_error(GOT_ERR_NO_SPACE);
> -			goto done;
> -		}
> -	}
> -	while (len > 0 && initial_content_stripped[len - 1] == '\n') {
> -		initial_content_stripped[len - 1] = '\0';
> -		len--;
> -	}
> -
> -	logmsg_len = st2.st_size;
> -	*logmsg = malloc(logmsg_len + 1);
> -	if (*logmsg == NULL)
> -		return got_error_from_errno("malloc");
> -	(*logmsg)[0] = '\0';
> -
> -	fp = fopen(logmsg_path, "re");
> -	if (fp == NULL) {
> -		err = got_error_from_errno("fopen");
> -		goto done;
> -	}
> -
> -	len = 0;
> -	while ((linelen = getline(&line, &linesize, fp)) != -1) {
> -		if ((line[0] == '#' || (len == 0 && line[0] == '\n')))
> -			continue; /* remove comments and leading empty lines */
> -		len = strlcat(*logmsg, line, logmsg_len + 1);
> -		if (len >= logmsg_len + 1) {
> -			err = got_error(GOT_ERR_NO_SPACE);
> -			goto done;
> -		}
> -	}
> -	free(line);
> -	if (ferror(fp)) {
> -		err = got_ferror(fp, GOT_ERR_IO);
> -		goto done;
> -	}
> -	while (len > 0 && (*logmsg)[len - 1] == '\n') {
> -		(*logmsg)[len - 1] = '\0';
> -		len--;
> -	}
> -
> -	if (len == 0) {
> -		err = got_error_msg(GOT_ERR_COMMIT_MSG_EMPTY,
> -		    "commit message cannot be empty, aborting");
> -		goto done;
> -	}
> -	if (require_modification &&
> -	    strcmp(*logmsg, initial_content_stripped) == 0)
> -		err = got_error_msg(GOT_ERR_COMMIT_MSG_EMPTY,
> -		    "no changes made to commit message, aborting");
> -done:
> -	free(initial_content_stripped);
> -	free(buf);
> -	if (fp && fclose(fp) == EOF && err == NULL)
> -		err = got_error_from_errno("fclose");
> -	if (err) {
> -		free(*logmsg);
> -		*logmsg = NULL;
> -	}
> -	return err;
> -}
> -
> -static const struct got_error *
> -collect_import_msg(char **logmsg, char **logmsg_path, const char *editor,
> -    const char *path_dir, const char *branch_name)
> -{
> -	char *initial_content = NULL;
> -	const struct got_error *err = NULL;
> -	int initial_content_len;
> -	int fd = -1;
> -
> -	initial_content_len = asprintf(&initial_content,
> -	    "\n# %s to be imported to branch %s\n", path_dir,
> -	    branch_name);
> -	if (initial_content_len == -1)
> -		return got_error_from_errno("asprintf");
> -
> -	err = got_opentemp_named_fd(logmsg_path, &fd,
> -	    GOT_TMPDIR_STR "/got-importmsg");
> -	if (err)
> -		goto done;
> -
> -	if (write(fd, initial_content, initial_content_len) == -1) {
> -		err = got_error_from_errno2("write", *logmsg_path);
> -		goto done;
> -	}
> -
> -	err = edit_logmsg(logmsg, editor, *logmsg_path, initial_content,
> -	    initial_content_len, 1);
> -done:
> -	if (fd != -1 && close(fd) == -1 && err == NULL)
> -		err = got_error_from_errno2("close", *logmsg_path);
> -	free(initial_content);
> -	if (err) {
> -		free(*logmsg_path);
> -		*logmsg_path = NULL;
> -	}
> -	return err;
> -}
> -
> -static const struct got_error *
> -import_progress(void *arg, const char *path)
> -{
> -	printf("A  %s\n", path);
> -	return NULL;
> -}
> -
> -static int
> -valid_author(const char *author)
> -{
> -	/*
> -	 * Really dumb email address check; we're only doing this to
> -	 * avoid git's object parser breaking on commits we create.
> -	 */
> -	while (*author && *author != '<')
> -		author++;
> -	if (*author != '<')
> -		return 0;
> -	while (*author && *author != '@')
> -		author++;
> -	if (*author != '@')
> -		return 0;
> -	while (*author && *author != '>')
> -		author++;
> -	return *author == '>';
> -}
> -
> -static const struct got_error *
> -get_author(char **author, struct got_repository *repo,
> -    struct got_worktree *worktree)
> -{
> -	const struct got_error *err = NULL;
> -	const char *got_author = NULL, *name, *email;
> -	const struct got_gotconfig *worktree_conf = NULL, *repo_conf = NULL;
> -
> -	*author = NULL;
> -
> -	if (worktree)
> -		worktree_conf = got_worktree_get_gotconfig(worktree);
> -	repo_conf = got_repo_get_gotconfig(repo);
> -
> -	/*
> -	 * Priority of potential author information sources, from most
> -	 * significant to least significant:
> -	 * 1) work tree's .got/got.conf file
> -	 * 2) repository's got.conf file
> -	 * 3) repository's git config file
> -	 * 4) environment variables
> -	 * 5) global git config files (in user's home directory or /etc)
> -	 */
> -
> -	if (worktree_conf)
> -		got_author = got_gotconfig_get_author(worktree_conf);
> -	if (got_author == NULL)
> -		got_author = got_gotconfig_get_author(repo_conf);
> -	if (got_author == NULL) {
> -		name = got_repo_get_gitconfig_author_name(repo);
> -		email = got_repo_get_gitconfig_author_email(repo);
> -		if (name && email) {
> -			if (asprintf(author, "%s <%s>", name, email) == -1)
> -				return got_error_from_errno("asprintf");
> -			return NULL;
> -		}
> -
> -		got_author = getenv("GOT_AUTHOR");
> -		if (got_author == NULL) {
> -			name = got_repo_get_global_gitconfig_author_name(repo);
> -			email = got_repo_get_global_gitconfig_author_email(
> -			    repo);
> -			if (name && email) {
> -				if (asprintf(author, "%s <%s>", name, email)
> -				    == -1)
> -					return got_error_from_errno("asprintf");
> -				return NULL;
> -			}
> -			/* TODO: Look up user in password database? */
> -			return got_error(GOT_ERR_COMMIT_NO_AUTHOR);
> -		}
> -	}
> -
> -	*author = strdup(got_author);
> -	if (*author == NULL)
> -		return got_error_from_errno("strdup");
> -
> -	if (!valid_author(*author)) {
> -		err = got_error_fmt(GOT_ERR_COMMIT_NO_EMAIL, "%s", *author);
> -		free(*author);
> -		*author = NULL;
> -	}
> -	return err;
> -}
> -
> -static const struct got_error *
>  get_allowed_signers(char **allowed_signers, struct got_repository *repo,
>      struct got_worktree *worktree)
>  {
> @@ -736,251 +418,6 @@ get_signer_id(char **signer_id, struct got_repository 
>  	return NULL;
>  }
>  
> -static const struct got_error *
> -get_gitconfig_path(char **gitconfig_path)
> -{
> -	const char *homedir = getenv("HOME");
> -
> -	*gitconfig_path = NULL;
> -	if (homedir) {
> -		if (asprintf(gitconfig_path, "%s/.gitconfig", homedir) == -1)
> -			return got_error_from_errno("asprintf");
> -
> -	}
> -	return NULL;
> -}
> -
> -static const struct got_error *
> -cmd_import(int argc, char *argv[])
> -{
> -	const struct got_error *error = NULL;
> -	char *path_dir = NULL, *repo_path = NULL, *logmsg = NULL;
> -	char *gitconfig_path = NULL, *editor = NULL, *author = NULL;
> -	const char *branch_name = "main";
> -	char *refname = NULL, *id_str = NULL, *logmsg_path = NULL;
> -	struct got_repository *repo = NULL;
> -	struct got_reference *branch_ref = NULL, *head_ref = NULL;
> -	struct got_object_id *new_commit_id = NULL;
> -	int ch;
> -	struct got_pathlist_head ignores;
> -	struct got_pathlist_entry *pe;
> -	int preserve_logmsg = 0;
> -	int *pack_fds = NULL;
> -
> -	TAILQ_INIT(&ignores);
> -
> -	while ((ch = getopt(argc, argv, "b:m:r:I:")) != -1) {
> -		switch (ch) {
> -		case 'b':
> -			branch_name = optarg;
> -			break;
> -		case 'm':
> -			logmsg = strdup(optarg);
> -			if (logmsg == NULL) {
> -				error = got_error_from_errno("strdup");
> -				goto done;
> -			}
> -			break;
> -		case 'r':
> -			repo_path = realpath(optarg, NULL);
> -			if (repo_path == NULL) {
> -				error = got_error_from_errno2("realpath",
> -				    optarg);
> -				goto done;
> -			}
> -			break;
> -		case 'I':
> -			if (optarg[0] == '\0')
> -				break;
> -			error = got_pathlist_insert(&pe, &ignores, optarg,
> -			    NULL);
> -			if (error)
> -				goto done;
> -			break;
> -		default:
> -			usage_import();
> -			/* NOTREACHED */
> -		}
> -	}
> -
> -	argc -= optind;
> -	argv += optind;
> -
> -#ifndef PROFILE
> -	if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd "
> -	    "unveil",
> -	    NULL) == -1)
> -		err(1, "pledge");
> -#endif
> -	if (argc != 1)
> -		usage_import();
> -
> -	if (repo_path == NULL) {
> -		repo_path = getcwd(NULL, 0);
> -		if (repo_path == NULL)
> -			return got_error_from_errno("getcwd");
> -	}
> -	got_path_strip_trailing_slashes(repo_path);
> -	error = get_gitconfig_path(&gitconfig_path);
> -	if (error)
> -		goto done;
> -	error = got_repo_pack_fds_open(&pack_fds);
> -	if (error != NULL)
> -		goto done;
> -	error = got_repo_open(&repo, repo_path, gitconfig_path, pack_fds);
> -	if (error)
> -		goto done;
> -
> -	error = get_author(&author, repo, NULL);
> -	if (error)
> -		return error;
> -
> -	/*
> -	 * Don't let the user create a branch name with a leading '-'.
> -	 * While technically a valid reference name, this case is usually
> -	 * an unintended typo.
> -	 */
> -	if (branch_name[0] == '-')
> -		return got_error_path(branch_name, GOT_ERR_REF_NAME_MINUS);
> -
> -	if (asprintf(&refname, "refs/heads/%s", branch_name) == -1) {
> -		error = got_error_from_errno("asprintf");
> -		goto done;
> -	}
> -
> -	error = got_ref_open(&branch_ref, repo, refname, 0);
> -	if (error) {
> -		if (error->code != GOT_ERR_NOT_REF)
> -			goto done;
> -	} else {
> -		error = got_error_msg(GOT_ERR_BRANCH_EXISTS,
> -		    "import target branch already exists");
> -		goto done;
> -	}
> -
> -	path_dir = realpath(argv[0], NULL);
> -	if (path_dir == NULL) {
> -		error = got_error_from_errno2("realpath", argv[0]);
> -		goto done;
> -	}
> -	got_path_strip_trailing_slashes(path_dir);
> -
> -	/*
> -	 * unveil(2) traverses exec(2); if an editor is used we have
> -	 * to apply unveil after the log message has been written.
> -	 */
> -	if (logmsg == NULL || strlen(logmsg) == 0) {
> -		error = get_editor(&editor);
> -		if (error)
> -			goto done;
> -		free(logmsg);
> -		error = collect_import_msg(&logmsg, &logmsg_path, editor,
> -		    path_dir, refname);
> -		if (error) {
> -			if (error->code != GOT_ERR_COMMIT_MSG_EMPTY &&
> -			    logmsg_path != NULL)
> -				preserve_logmsg = 1;
> -			goto done;
> -		}
> -	}
> -
> -	if (unveil(path_dir, "r") != 0) {
> -		error = got_error_from_errno2("unveil", path_dir);
> -		if (logmsg_path)
> -			preserve_logmsg = 1;
> -		goto done;
> -	}
> -
> -	error = apply_unveil(got_repo_get_path(repo), 0, NULL);
> -	if (error) {
> -		if (logmsg_path)
> -			preserve_logmsg = 1;
> -		goto done;
> -	}
> -
> -	error = got_repo_import(&new_commit_id, path_dir, logmsg,
> -	    author, &ignores, repo, import_progress, NULL);
> -	if (error) {
> -		if (logmsg_path)
> -			preserve_logmsg = 1;
> -		goto done;
> -	}
> -
> -	error = got_ref_alloc(&branch_ref, refname, new_commit_id);
> -	if (error) {
> -		if (logmsg_path)
> -			preserve_logmsg = 1;
> -		goto done;
> -	}
> -
> -	error = got_ref_write(branch_ref, repo);
> -	if (error) {
> -		if (logmsg_path)
> -			preserve_logmsg = 1;
> -		goto done;
> -	}
> -
> -	error = got_object_id_str(&id_str, new_commit_id);
> -	if (error) {
> -		if (logmsg_path)
> -			preserve_logmsg = 1;
> -		goto done;
> -	}
> -
> -	error = got_ref_open(&head_ref, repo, GOT_REF_HEAD, 0);
> -	if (error) {
> -		if (error->code != GOT_ERR_NOT_REF) {
> -			if (logmsg_path)
> -				preserve_logmsg = 1;
> -			goto done;
> -		}
> -
> -		error = got_ref_alloc_symref(&head_ref, GOT_REF_HEAD,
> -		    branch_ref);
> -		if (error) {
> -			if (logmsg_path)
> -				preserve_logmsg = 1;
> -			goto done;
> -		}
> -
> -		error = got_ref_write(head_ref, repo);
> -		if (error) {
> -			if (logmsg_path)
> -				preserve_logmsg = 1;
> -			goto done;
> -		}
> -	}
> -
> -	printf("Created branch %s with commit %s\n",
> -	    got_ref_get_name(branch_ref), id_str);
> -done:
> -	if (pack_fds) {
> -		const struct got_error *pack_err =
> -		    got_repo_pack_fds_close(pack_fds);
> -		if (error == NULL)
> -			error = pack_err;
> -	}
> -	if (preserve_logmsg) {
> -		fprintf(stderr, "%s: log message preserved in %s\n",
> -		    getprogname(), logmsg_path);
> -	} else if (logmsg_path && unlink(logmsg_path) == -1 && error == NULL)
> -		error = got_error_from_errno2("unlink", logmsg_path);
> -	free(logmsg);
> -	free(logmsg_path);
> -	free(repo_path);
> -	free(editor);
> -	free(refname);
> -	free(new_commit_id);
> -	free(id_str);
> -	free(author);
> -	free(gitconfig_path);
> -	if (branch_ref)
> -		got_ref_close(branch_ref);
> -	if (head_ref)
> -		got_ref_close(head_ref);
> -	return error;
> -}
> -
>  __dead static void
>  usage_clone(void)
>  {
> @@ -7169,10 +6606,10 @@ get_tag_message(char **tagmsg, char **tagmsg_path, con
>  		goto done;
>  	}
>  
> -	err = get_editor(&editor);
> +	err = got_logmsg_get_editor(&editor);
>  	if (err)
>  		goto done;
> -	err = edit_logmsg(tagmsg, editor, *tagmsg_path, initial_content,
> +	err = got_logmsg_edit(tagmsg, editor, *tagmsg_path, initial_content,
>  	    initial_content_len, 1);
>  done:
>  	free(initial_content);
> @@ -7460,7 +6897,7 @@ cmd_tag(int argc, char *argv[])
>  		error = list_tags(repo, tag_name, verify_tags, allowed_signers,
>  		    revoked_signers, verbosity);
>  	} else {
> -		error = get_gitconfig_path(&gitconfig_path);
> +		error = got_repo_get_gitconfig_path(&gitconfig_path);
>  		if (error)
>  			goto done;
>  		error = got_repo_open(&repo, repo_path, gitconfig_path,
> @@ -7468,7 +6905,7 @@ cmd_tag(int argc, char *argv[])
>  		if (error != NULL)
>  			goto done;
>  
> -		error = get_author(&tagger, repo, worktree);
> +		error = got_logmsg_get_author(&tagger, repo, worktree);
>  		if (error)
>  			goto done;
>  		if (signer_id == NULL) {
> @@ -8446,8 +7883,8 @@ collect_commit_logmsg(struct got_pathlist_head *commit
>  		    got_commitable_get_path(ct));
>  	}
>  
> -	err = edit_logmsg(logmsg, a->editor, a->logmsg_path, initial_content,
> -	    initial_content_len, a->prepared_log ? 0 : 1);
> +	err = got_logmsg_edit(logmsg, a->editor, a->logmsg_path,
> +	    initial_content, initial_content_len, a->prepared_log ? 0 : 1);
>  done:
>  	free(initial_content);
>  	free(template);
> @@ -8550,7 +7987,7 @@ cmd_commit(int argc, char *argv[])
>  	if (error)
>  		goto done;
>  
> -	error = get_gitconfig_path(&gitconfig_path);
> +	error = got_repo_get_gitconfig_path(&gitconfig_path);
>  	if (error)
>  		goto done;
>  	error = got_repo_open(&repo, got_worktree_get_repo_path(worktree),
> @@ -8566,7 +8003,7 @@ cmd_commit(int argc, char *argv[])
>  		goto done;
>  	}
>  
> -	error = get_author(&author, repo, worktree);
> +	error = got_logmsg_get_author(&author, repo, worktree);
>  	if (error)
>  		return error;
>  
> @@ -8575,7 +8012,7 @@ cmd_commit(int argc, char *argv[])
>  	 * to apply unveil after the log message has been written.
>  	 */
>  	if (logmsg == NULL || strlen(logmsg) == 0)
> -		error = get_editor(&editor);
> +		error = got_logmsg_get_editor(&editor);
>  	else
>  		error = apply_unveil(got_repo_get_path(repo), 0,
>  		    got_worktree_get_root_path(worktree));
> @@ -10665,11 +10102,11 @@ histedit_edit_logmsg(struct got_histedit_list_entry *h
>  	write(fd, logmsg, logmsg_len);
>  	close(fd);
>  
> -	err = get_editor(&editor);
> +	err = got_logmsg_get_editor(&editor);
>  	if (err)
>  		goto done;
>  
> -	err = edit_logmsg(&hle->logmsg, editor, logmsg_path, logmsg,
> +	err = got_logmsg_edit(&hle->logmsg, editor, logmsg_path, logmsg,
>  	    logmsg_len, 0);
>  	if (err) {
>  		if (err->code != GOT_ERR_COMMIT_MSG_EMPTY)
> @@ -10860,11 +10297,11 @@ histedit_run_editor(struct got_histedit_list *histedit
>  	char *editor;
>  	FILE *f = NULL;
>  
> -	err = get_editor(&editor);
> +	err = got_logmsg_get_editor(&editor);
>  	if (err)
>  		return err;
>  
> -	if (spawn_editor(editor, path) == -1) {
> +	if (got_logmsg_spawn_editor(editor, path) == -1) {
>  		err = got_error_from_errno("failed spawning editor");
>  		goto done;
>  	}
> @@ -12057,7 +11494,7 @@ cmd_merge(int argc, char *argv[])
>  		goto done; /* nothing else to do */
>  	}
>  
> -	error = get_author(&author, repo, worktree);
> +	error = got_logmsg_get_author(&author, repo, worktree);
>  	if (error)
>  		goto done;
>  
> blob - bf9729a9216142455edfd253fb05cd98c0b4b1f1
> blob + 183759b3c11df3e317479aeda4517a504cb470a1
> --- gotadmin/Makefile
> +++ gotadmin/Makefile
> @@ -9,7 +9,7 @@ SRCS=		gotadmin.c \
>  		object_idset.c object_parse.c opentemp.c pack.c pack_create.c \
>  		path.c privsep.c reference.c repository.c repository_admin.c \
>  		worktree_open.c sha1.c bloom.c murmurhash2.c ratelimit.c \
> -		sigs.c buf.c date.c
> +		sigs.c buf.c date.c logmsg.c
>  MAN =		${PROG}.1
>  
>  CPPFLAGS = -I${.CURDIR}/../include -I${.CURDIR}/../lib
> blob - 6205dfc5bd0505463ccfca983492ba58b3ac92fe
> blob + a8978bd21c811ade17a8000ebef9ab34d1e82d91
> --- gotadmin/gotadmin.1
> +++ gotadmin/gotadmin.1
> @@ -60,7 +60,7 @@ Create a new empty repository at the specified
>  After
>  .Cm gotadmin init ,
>  the
> -.Cm got import
> +.Cm gotadmin import
>  command must be used to populate the empty repository before
>  .Cm got checkout
>  can be used.
> @@ -83,6 +83,67 @@ If this directory is a
>  .Xr got 1
>  work tree, use the repository path associated with this work tree.
>  .El
> +.Tg im
> +.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
> +.Dl Pq alias: Cm im
> +Create an initial commit in a repository from the file hierarchy
> +within the specified
> +.Ar directory .
> +The created commit will not have any parent commits, i.e. it will be a
> +root commit.
> +Also create a new reference which provides a branch name for the newly
> +created commit.
> +Show the path of each imported file to indicate progress.
> +.Pp
> +The
> +.Cm gotadmin import
> +command requires the
> +.Ev GOT_AUTHOR
> +environment variable to be set,
> +unless an author has been configured in
> +.Xr got.conf 5
> +or Git's
> +.Dv user.name
> +and
> +.Dv user.email
> +configuration settings can be obtained from the repository's
> +.Pa .git/config
> +file or from Git's global
> +.Pa ~/.gitconfig
> +configuration file.
> +.Pp
> +The options for
> +.Cm gotadmin import
> +are as follows:
> +.Bl -tag -width Ds
> +.It Fl b Ar branch
> +Create the specified
> +.Ar branch
> +instead of creating the default branch
> +.Dq main .
> +Use of this option is required if the
> +.Dq main
> +branch already exists.
> +.It Fl m Ar message
> +Use the specified log message when creating the new commit.
> +Without the
> +.Fl m
> +option,
> +.Cm gotadmin import
> +opens a temporary file in an editor where a log message can be written.
> +.It Fl r Ar repository-path
> +Use the repository at the specified path.
> +If not specified, assume the repository is located at or above the current
> +working directory.
> +.It Fl I Ar pattern
> +Ignore files or directories with a name which matches the specified
> +.Ar pattern .
> +This option may be specified multiple times to build a list of ignore patterns.
> +The
> +.Ar pattern
> +follows the globbing rules documented in
> +.Xr glob 7 .
> +.El
>  .It Cm pack Oo Fl a Oc Oo Fl r Ar repository-path Oc Oo Fl x Ar reference Oc Oo Fl q Oc Op Ar reference ...
>  Generate a new pack file and a corresponding pack file index.
>  By default, add any loose objects which are reachable via any references
> blob - f4d2b3013d6751cf1612acb25b8552e3910f05f5
> blob + 4c3cddb38fc5ed35941da801a1d3e0f057bd0b80
> --- gotadmin/gotadmin.c
> +++ gotadmin/gotadmin.c
> @@ -43,6 +43,7 @@
>  #include "got_privsep.h"
>  #include "got_opentemp.h"
>  #include "got_worktree.h"
> +#include "got_logmsg.h"
>  
>  #ifndef nitems
>  #define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
> @@ -80,6 +81,7 @@ struct gotadmin_cmd {
>  
>  __dead static void	usage(int, int);
>  __dead static void	usage_init(void);
> +__dead static void	usage_import(void);
>  __dead static void	usage_info(void);
>  __dead static void	usage_pack(void);
>  __dead static void	usage_indexpack(void);
> @@ -87,6 +89,7 @@ __dead static void	usage_listpack(void);
>  __dead static void	usage_cleanup(void);
>  
>  static const struct got_error*		cmd_init(int, char *[]);
> +static const struct got_error*		cmd_import(int, char *[]);
>  static const struct got_error*		cmd_info(int, char *[]);
>  static const struct got_error*		cmd_pack(int, char *[]);
>  static const struct got_error*		cmd_indexpack(int, char *[]);
> @@ -95,6 +98,7 @@ static const struct got_error*		cmd_cleanup(int, char 
>  
>  static const struct gotadmin_cmd gotadmin_commands[] = {
>  	{ "init",	cmd_init,	usage_init,	"" },
> +	{ "import",	cmd_import,	usage_import,	"im" },
>  	{ "info",	cmd_info,	usage_info,	"" },
>  	{ "pack",	cmd_pack,	usage_pack,	"" },
>  	{ "indexpack",	cmd_indexpack,	usage_indexpack,"ix" },
> @@ -321,7 +325,291 @@ done:
>  	return error;
>  }
>  
> +__dead static void
> +usage_import(void)
> +{
> +	fprintf(stderr, "usage: %s import [-b branch] [-m message] "
> +	    "[-r repository-path] [-I pattern] path\n", getprogname());
> +	exit(1);
> +}
> +
>  static const struct got_error *
> +collect_import_msg(char **logmsg, char **logmsg_path, const char *editor,
> +    const char *path_dir, const char *branch_name)
> +{
> +	char *initial_content = NULL;
> +	const struct got_error *err = NULL;
> +	int initial_content_len;
> +	int fd = -1;
> +
> +	initial_content_len = asprintf(&initial_content,
> +	    "\n# %s to be imported to branch %s\n", path_dir,
> +	    branch_name);
> +	if (initial_content_len == -1)
> +		return got_error_from_errno("asprintf");
> +
> +	err = got_opentemp_named_fd(logmsg_path, &fd,
> +	    GOT_TMPDIR_STR "/got-importmsg");
> +	if (err)
> +		goto done;
> +
> +	if (write(fd, initial_content, initial_content_len) == -1) {
> +		err = got_error_from_errno2("write", *logmsg_path);
> +		goto done;
> +	}
> +
> +	err = got_logmsg_edit(logmsg, editor, *logmsg_path, initial_content,
> +	    initial_content_len, 1);
> +done:
> +	if (fd != -1 && close(fd) == -1 && err == NULL)
> +		err = got_error_from_errno2("close", *logmsg_path);
> +	free(initial_content);
> +	if (err) {
> +		free(*logmsg_path);
> +		*logmsg_path = NULL;
> +	}
> +	return err;
> +}
> +
> +static const struct got_error *
> +import_progress(void *arg, const char *path)
> +{
> +	printf("A  %s\n", path);
> +	return NULL;
> +}
> +
> +static const struct got_error *
> +cmd_import(int argc, char *argv[])
> +{
> +	const struct got_error *error = NULL;
> +	char *path_dir = NULL, *repo_path = NULL, *logmsg = NULL;
> +	char *gitconfig_path = NULL, *editor = NULL, *author = NULL;
> +	const char *branch_name = "main";
> +	char *refname = NULL, *id_str = NULL, *logmsg_path = NULL;
> +	struct got_repository *repo = NULL;
> +	struct got_reference *branch_ref = NULL, *head_ref = NULL;
> +	struct got_object_id *new_commit_id = NULL;
> +	int ch;
> +	struct got_pathlist_head ignores;
> +	struct got_pathlist_entry *pe;
> +	int preserve_logmsg = 0;
> +	int *pack_fds = NULL;
> +
> +	TAILQ_INIT(&ignores);
> +
> +	while ((ch = getopt(argc, argv, "b:m:r:I:")) != -1) {
> +		switch (ch) {
> +		case 'b':
> +			branch_name = optarg;
> +			break;
> +		case 'm':
> +			logmsg = strdup(optarg);
> +			if (logmsg == NULL) {
> +				error = got_error_from_errno("strdup");
> +				goto done;
> +			}
> +			break;
> +		case 'r':
> +			repo_path = realpath(optarg, NULL);
> +			if (repo_path == NULL) {
> +				error = got_error_from_errno2("realpath",
> +				    optarg);
> +				goto done;
> +			}
> +			break;
> +		case 'I':
> +			if (optarg[0] == '\0')
> +				break;
> +			error = got_pathlist_insert(&pe, &ignores, optarg,
> +			    NULL);
> +			if (error)
> +				goto done;
> +			break;
> +		default:
> +			usage_import();
> +			/* NOTREACHED */
> +		}
> +	}
> +
> +	argc -= optind;
> +	argv += optind;
> +
> +#ifndef PROFILE
> +	if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd "
> +	    "unveil",
> +	    NULL) == -1)
> +		err(1, "pledge");
> +#endif
> +	if (argc != 1)
> +		usage_import();
> +
> +	if (repo_path == NULL) {
> +		repo_path = getcwd(NULL, 0);
> +		if (repo_path == NULL)
> +			return got_error_from_errno("getcwd");
> +	}
> +	got_path_strip_trailing_slashes(repo_path);
> +	error = got_repo_get_gitconfig_path(&gitconfig_path);
> +	if (error)
> +		goto done;
> +	error = got_repo_pack_fds_open(&pack_fds);
> +	if (error != NULL)
> +		goto done;
> +	error = got_repo_open(&repo, repo_path, gitconfig_path, pack_fds);
> +	if (error)
> +		goto done;
> +
> +	error = got_logmsg_get_author(&author, repo, NULL);
> +	if (error)
> +		return error;
> +
> +	/*
> +	 * Don't let the user create a branch name with a leading '-'.
> +	 * While technically a valid reference name, this case is usually
> +	 * an unintended typo.
> +	 */
> +	if (branch_name[0] == '-')
> +		return got_error_path(branch_name, GOT_ERR_REF_NAME_MINUS);
> +
> +	if (asprintf(&refname, "refs/heads/%s", branch_name) == -1) {
> +		error = got_error_from_errno("asprintf");
> +		goto done;
> +	}
> +
> +	error = got_ref_open(&branch_ref, repo, refname, 0);
> +	if (error) {
> +		if (error->code != GOT_ERR_NOT_REF)
> +			goto done;
> +	} else {
> +		error = got_error_msg(GOT_ERR_BRANCH_EXISTS,
> +		    "import target branch already exists");
> +		goto done;
> +	}
> +
> +	path_dir = realpath(argv[0], NULL);
> +	if (path_dir == NULL) {
> +		error = got_error_from_errno2("realpath", argv[0]);
> +		goto done;
> +	}
> +	got_path_strip_trailing_slashes(path_dir);
> +
> +	/*
> +	 * unveil(2) traverses exec(2); if an editor is used we have
> +	 * to apply unveil after the log message has been written.
> +	 */
> +	if (logmsg == NULL || strlen(logmsg) == 0) {
> +		error = got_logmsg_get_editor(&editor);
> +		if (error)
> +			goto done;
> +		free(logmsg);
> +		error = collect_import_msg(&logmsg, &logmsg_path, editor,
> +		    path_dir, refname);
> +		if (error) {
> +			if (error->code != GOT_ERR_COMMIT_MSG_EMPTY &&
> +			    logmsg_path != NULL)
> +				preserve_logmsg = 1;
> +			goto done;
> +		}
> +	}
> +
> +	if (unveil(path_dir, "r") != 0) {
> +		error = got_error_from_errno2("unveil", path_dir);
> +		if (logmsg_path)
> +			preserve_logmsg = 1;
> +		goto done;
> +	}
> +
> +	error = apply_unveil(got_repo_get_path(repo), 0);
> +	if (error) {
> +		if (logmsg_path)
> +			preserve_logmsg = 1;
> +		goto done;
> +	}
> +
> +	error = got_repo_import(&new_commit_id, path_dir, logmsg,
> +	    author, &ignores, repo, import_progress, NULL);
> +	if (error) {
> +		if (logmsg_path)
> +			preserve_logmsg = 1;
> +		goto done;
> +	}
> +
> +	error = got_ref_alloc(&branch_ref, refname, new_commit_id);
> +	if (error) {
> +		if (logmsg_path)
> +			preserve_logmsg = 1;
> +		goto done;
> +	}
> +
> +	error = got_ref_write(branch_ref, repo);
> +	if (error) {
> +		if (logmsg_path)
> +			preserve_logmsg = 1;
> +		goto done;
> +	}
> +
> +	error = got_object_id_str(&id_str, new_commit_id);
> +	if (error) {
> +		if (logmsg_path)
> +			preserve_logmsg = 1;
> +		goto done;
> +	}
> +
> +	error = got_ref_open(&head_ref, repo, GOT_REF_HEAD, 0);
> +	if (error) {
> +		if (error->code != GOT_ERR_NOT_REF) {
> +			if (logmsg_path)
> +				preserve_logmsg = 1;
> +			goto done;
> +		}
> +
> +		error = got_ref_alloc_symref(&head_ref, GOT_REF_HEAD,
> +		    branch_ref);
> +		if (error) {
> +			if (logmsg_path)
> +				preserve_logmsg = 1;
> +			goto done;
> +		}
> +
> +		error = got_ref_write(head_ref, repo);
> +		if (error) {
> +			if (logmsg_path)
> +				preserve_logmsg = 1;
> +			goto done;
> +		}
> +	}
> +
> +	printf("Created branch %s with commit %s\n",
> +	    got_ref_get_name(branch_ref), id_str);
> +done:
> +	if (pack_fds) {
> +		const struct got_error *pack_err =
> +		    got_repo_pack_fds_close(pack_fds);
> +		if (error == NULL)
> +			error = pack_err;
> +	}
> +	if (preserve_logmsg) {
> +		fprintf(stderr, "%s: log message preserved in %s\n",
> +		    getprogname(), logmsg_path);
> +	} else if (logmsg_path && unlink(logmsg_path) == -1 && error == NULL)
> +		error = got_error_from_errno2("unlink", logmsg_path);
> +	free(logmsg);
> +	free(logmsg_path);
> +	free(repo_path);
> +	free(editor);
> +	free(refname);
> +	free(new_commit_id);
> +	free(id_str);
> +	free(author);
> +	free(gitconfig_path);
> +	if (branch_ref)
> +		got_ref_close(branch_ref);
> +	if (head_ref)
> +		got_ref_close(head_ref);
> +	return error;
> +}
> +
> +static const struct got_error *
>  cmd_info(int argc, char *argv[])
>  {
>  	const struct got_error *error = NULL;
> blob - /dev/null
> blob + 2764cd763f3cdb2cf63c52f1c4e7e6d1fa83af98 (mode 644)
> --- /dev/null
> +++ include/got_logmsg.h
> @@ -0,0 +1,25 @@
> +/*
> + * 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.
> + */
> +
> +const struct got_error *got_logmsg_get_author(char **author,
> +    struct got_repository *repo, struct got_worktree *worktree);
> +
> +const struct got_error *got_logmsg_get_editor(char **);
> +int got_logmsg_spawn_editor(const char *editor, const char *file);
> +
> +const struct got_error *got_logmsg_edit(char **logmsg, const char *editor,
> +    const char *logmsg_path, const char *initial_content,
> +    size_t initial_content_len, int require_modification);
> blob - dea6dd81d267dfa92571a33f5c7559726bab8d8b
> blob + 48c19f5f94255edff5e2bd2f72f5f7cb6251f264
> --- include/got_repository.h
> +++ include/got_repository.h
> @@ -54,6 +54,9 @@ const char *got_repo_get_gitconfig_owner(struct got_re
>  void got_repo_get_gitconfig_extensions(char ***, int *,
>      struct got_repository *);
>  
> +/* Obtain the path to the user's .gitconfig file. Caller must free(3). */
> +const struct got_error *got_repo_get_gitconfig_path(char **);
> +
>  /* Information about one remote repository. */
>  struct got_remote_repo {
>  	char *name;
> blob - /dev/null
> blob + e5910e4fa816e28ec67a826ee5aea57fe7f43e6f (mode 644)
> --- /dev/null
> +++ lib/logmsg.c
> @@ -0,0 +1,297 @@
> +/*
> + * 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.
> + */
> +
> +#include <sys/stat.h>
> +#include <sys/wait.h>
> +#include <sys/queue.h>
> +
> +#include <errno.h>
> +#include <signal.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <unistd.h>
> +
> +#include "got_error.h"
> +#include "got_cancel.h"
> +#include "got_repository.h"
> +#include "got_worktree.h"
> +#include "got_logmsg.h"
> +#include "got_path.h"
> +#include "got_gotconfig.h"
> +
> +static int
> +valid_author(const char *author)
> +{
> +	/*
> +	 * Really dumb email address check; we're only doing this to
> +	 * avoid git's object parser breaking on commits we create.
> +	 */
> +	while (*author && *author != '<')
> +		author++;
> +	if (*author != '<')
> +		return 0;
> +	while (*author && *author != '@')
> +		author++;
> +	if (*author != '@')
> +		return 0;
> +	while (*author && *author != '>')
> +		author++;
> +	return *author == '>';
> +}
> +
> +const struct got_error *
> +got_logmsg_get_author(char **author, struct got_repository *repo,
> +    struct got_worktree *worktree)
> +{
> +	const struct got_error *err = NULL;
> +	const char *got_author = NULL, *name, *email;
> +	const struct got_gotconfig *worktree_conf = NULL, *repo_conf = NULL;
> +
> +	*author = NULL;
> +
> +	if (worktree)
> +		worktree_conf = got_worktree_get_gotconfig(worktree);
> +	repo_conf = got_repo_get_gotconfig(repo);
> +
> +	/*
> +	 * Priority of potential author information sources, from most
> +	 * significant to least significant:
> +	 * 1) work tree's .got/got.conf file
> +	 * 2) repository's got.conf file
> +	 * 3) repository's git config file
> +	 * 4) environment variables
> +	 * 5) global git config files (in user's home directory or /etc)
> +	 */
> +
> +	if (worktree_conf)
> +		got_author = got_gotconfig_get_author(worktree_conf);
> +	if (got_author == NULL)
> +		got_author = got_gotconfig_get_author(repo_conf);
> +	if (got_author == NULL) {
> +		name = got_repo_get_gitconfig_author_name(repo);
> +		email = got_repo_get_gitconfig_author_email(repo);
> +		if (name && email) {
> +			if (asprintf(author, "%s <%s>", name, email) == -1)
> +				return got_error_from_errno("asprintf");
> +			return NULL;
> +		}
> +
> +		got_author = getenv("GOT_AUTHOR");
> +		if (got_author == NULL) {
> +			name = got_repo_get_global_gitconfig_author_name(repo);
> +			email = got_repo_get_global_gitconfig_author_email(
> +			    repo);
> +			if (name && email) {
> +				if (asprintf(author, "%s <%s>", name, email)
> +				    == -1)
> +					return got_error_from_errno("asprintf");
> +				return NULL;
> +			}
> +			/* TODO: Look up user in password database? */
> +			return got_error(GOT_ERR_COMMIT_NO_AUTHOR);
> +		}
> +	}
> +
> +	*author = strdup(got_author);
> +	if (*author == NULL)
> +		return got_error_from_errno("strdup");
> +
> +	if (!valid_author(*author)) {
> +		err = got_error_fmt(GOT_ERR_COMMIT_NO_EMAIL, "%s", *author);
> +		free(*author);
> +		*author = NULL;
> +	}
> +	return err;
> +}
> +
> +const struct got_error *
> +got_logmsg_get_editor(char **abspath)
> +{
> +	const struct got_error *err = NULL;
> +	const char *editor;
> +
> +	*abspath = NULL;
> +
> +	editor = getenv("VISUAL");
> +	if (editor == NULL)
> +		editor = getenv("EDITOR");
> +
> +	if (editor) {
> +		err = got_path_find_prog(abspath, editor);
> +		if (err)
> +			return err;
> +	}
> +
> +	if (*abspath == NULL) {
> +		*abspath = strdup("/bin/ed");
> +		if (*abspath == NULL)
> +			return got_error_from_errno("strdup");
> +	}
> +
> +	return NULL;
> +}
> +
> +int
> +got_logmsg_spawn_editor(const char *editor, const char *file)
> +{
> +	pid_t pid;
> +	sig_t sighup, sigint, sigquit;
> +	int st = -1;
> +
> +	sighup = signal(SIGHUP, SIG_IGN);
> +	sigint = signal(SIGINT, SIG_IGN);
> +	sigquit = signal(SIGQUIT, SIG_IGN);
> +
> +	switch (pid = fork()) {
> +	case -1:
> +		goto doneediting;
> +	case 0:
> +		execl(editor, editor, file, (char *)NULL);
> +		_exit(127);
> +	}
> +
> +	while (waitpid(pid, &st, 0) == -1)
> +		if (errno != EINTR)
> +			break;
> +
> +doneediting:
> +	(void)signal(SIGHUP, sighup);
> +	(void)signal(SIGINT, sigint);
> +	(void)signal(SIGQUIT, sigquit);
> +
> +	if (!WIFEXITED(st)) {
> +		errno = EINTR;
> +		return -1;
> +	}
> +
> +	return WEXITSTATUS(st);
> +}
> +
> +const struct got_error *
> +got_logmsg_edit(char **logmsg, const char *editor, const char *logmsg_path,
> +    const char *initial_content, size_t initial_content_len,
> +    int require_modification)
> +{
> +	const struct got_error *err = NULL;
> +	char *line = NULL;
> +	size_t linesize = 0;
> +	ssize_t linelen;
> +	struct stat st, st2;
> +	FILE *fp = NULL;
> +	size_t len, logmsg_len;
> +	char *initial_content_stripped = NULL, *buf = NULL, *s;
> +
> +	*logmsg = NULL;
> +
> +	if (stat(logmsg_path, &st) == -1)
> +		return got_error_from_errno2("stat", logmsg_path);
> +
> +	if (got_logmsg_spawn_editor(editor, logmsg_path) == -1)
> +		return got_error_from_errno("failed spawning editor");
> +
> +	if (stat(logmsg_path, &st2) == -1)
> +		return got_error_from_errno("stat");
> +
> +	if (require_modification &&
> +	    st.st_mtime == st2.st_mtime && st.st_size == st2.st_size)
> +		return got_error_msg(GOT_ERR_COMMIT_MSG_EMPTY,
> +		    "no changes made to commit message, aborting");
> +
> +	/*
> +	 * Set up a stripped version of the initial content without comments
> +	 * and blank lines. We need this in order to check if the message
> +	 * has in fact been edited.
> +	 */
> +	initial_content_stripped = malloc(initial_content_len + 1);
> +	if (initial_content_stripped == NULL)
> +		return got_error_from_errno("malloc");
> +	initial_content_stripped[0] = '\0';
> +
> +	buf = strdup(initial_content);
> +	if (buf == NULL) {
> +		err = got_error_from_errno("strdup");
> +		goto done;
> +	}
> +	s = buf;
> +	len = 0;
> +	while ((line = strsep(&s, "\n")) != NULL) {
> +		if ((line[0] == '#' || (len == 0 && line[0] == '\n')))
> +			continue; /* remove comments and leading empty lines */
> +		len = strlcat(initial_content_stripped, line,
> +		    initial_content_len + 1);
> +		if (len >= initial_content_len + 1) {
> +			err = got_error(GOT_ERR_NO_SPACE);
> +			goto done;
> +		}
> +	}
> +	while (len > 0 && initial_content_stripped[len - 1] == '\n') {
> +		initial_content_stripped[len - 1] = '\0';
> +		len--;
> +	}
> +
> +	logmsg_len = st2.st_size;
> +	*logmsg = malloc(logmsg_len + 1);
> +	if (*logmsg == NULL)
> +		return got_error_from_errno("malloc");
> +	(*logmsg)[0] = '\0';
> +
> +	fp = fopen(logmsg_path, "re");
> +	if (fp == NULL) {
> +		err = got_error_from_errno("fopen");
> +		goto done;
> +	}
> +
> +	len = 0;
> +	while ((linelen = getline(&line, &linesize, fp)) != -1) {
> +		if ((line[0] == '#' || (len == 0 && line[0] == '\n')))
> +			continue; /* remove comments and leading empty lines */
> +		len = strlcat(*logmsg, line, logmsg_len + 1);
> +		if (len >= logmsg_len + 1) {
> +			err = got_error(GOT_ERR_NO_SPACE);
> +			goto done;
> +		}
> +	}
> +	free(line);
> +	if (ferror(fp)) {
> +		err = got_ferror(fp, GOT_ERR_IO);
> +		goto done;
> +	}
> +	while (len > 0 && (*logmsg)[len - 1] == '\n') {
> +		(*logmsg)[len - 1] = '\0';
> +		len--;
> +	}
> +
> +	if (len == 0) {
> +		err = got_error_msg(GOT_ERR_COMMIT_MSG_EMPTY,
> +		    "commit message cannot be empty, aborting");
> +		goto done;
> +	}
> +	if (require_modification &&
> +	    strcmp(*logmsg, initial_content_stripped) == 0)
> +		err = got_error_msg(GOT_ERR_COMMIT_MSG_EMPTY,
> +		    "no changes made to commit message, aborting");
> +done:
> +	free(initial_content_stripped);
> +	free(buf);
> +	if (fp && fclose(fp) == EOF && err == NULL)
> +		err = got_error_from_errno("fclose");
> +	if (err) {
> +		free(*logmsg);
> +		*logmsg = NULL;
> +	}
> +	return err;
> +}
> blob - 4c93c601016c47cab8439703f5925c65095b4b7e
> blob + 9befcb7dc09ce5e8891cdb64f3541975366933c5
> --- lib/repository.c
> +++ lib/repository.c
> @@ -129,6 +129,20 @@ got_repo_get_gitconfig_extensions(char ***extensions, 
>  	*nextensions = repo->nextensions;
>  }
>  
> +const struct got_error *
> +got_repo_get_gitconfig_path(char **gitconfig_path)
> +{
> +	const char *homedir = getenv("HOME");
> +
> +	*gitconfig_path = NULL;
> +	if (homedir) {
> +		if (asprintf(gitconfig_path, "%s/.gitconfig", homedir) == -1)
> +			return got_error_from_errno("asprintf");
> +
> +	}
> +	return NULL;
> +}
> +
>  int
>  got_repo_is_bare(struct got_repository *repo)
>  {
> blob - 585486aa9bd3b5620681a9e4ccad89befbbaf36b
> blob + d506cf8b1f1ddc3c2631bbbc5e3ffa67314c3aec
> --- lib/worktree.c
> +++ lib/worktree.c
> @@ -364,12 +364,6 @@ done:
>  	return err;
>  }
>  
> -const struct got_gotconfig *
> -got_worktree_get_gotconfig(struct got_worktree *worktree)
> -{
> -	return worktree->gotconfig;
> -}
> -
>  static const struct got_error *
>  lock_worktree(struct got_worktree *worktree, int operation)
>  {
> blob - 529f4b6767b84a31e24d93b402c6a3c8226cdd74
> blob + f82e12b4fe0053656a74f49f11d00bbd552b4e77
> --- lib/worktree_open.c
> +++ lib/worktree_open.c
> @@ -335,3 +335,9 @@ got_worktree_get_path_prefix(struct got_worktree *work
>  {
>  	return worktree->path_prefix;
>  }
> +
> +const struct got_gotconfig *
> +got_worktree_get_gotconfig(struct got_worktree *worktree)
> +{
> +	return worktree->gotconfig;
> +}
> blob - 71d20c61b9666f8571b8996f1bb477f6e01a3c1f
> blob + e70021a604b6b1a96aad6f74843827f344d8671a
> --- regress/cmdline/import.sh
> +++ regress/cmdline/import.sh
> @@ -25,7 +25,7 @@ test_import_basic() {
>  	mkdir $testroot/tree
>  	make_test_tree $testroot/tree
>  
> -	got import -m 'init' -r $testroot/repo $testroot/tree \
> +	gotadmin import -m 'init' -r $testroot/repo $testroot/tree \
>  		> $testroot/stdout
>  	ret=$?
>  	if [ $ret -ne 0 ]; then
> @@ -146,7 +146,7 @@ test_import_requires_new_branch() {
>  	mkdir $testroot/tree
>  	make_test_tree $testroot/tree
>  
> -	got import -b master -m 'init' -r $testroot/repo $testroot/tree \
> +	gotadmin import -b master -m 'init' -r $testroot/repo $testroot/tree \
>  		> $testroot/stdout 2> $testroot/stderr
>  	ret=$?
>  	if [ $ret -eq 0 ]; then
> @@ -155,7 +155,7 @@ test_import_requires_new_branch() {
>  		return 1
>  	fi
>  
> -	echo "got: import target branch already exists" \
> +	echo "gotadmin: import target branch already exists" \
>  		> $testroot/stderr.expected
>  	cmp -s $testroot/stderr.expected $testroot/stderr
>  	ret=$?
> @@ -165,8 +165,8 @@ test_import_requires_new_branch() {
>  		return 1
>  	fi
>  
> -	got import -b newbranch -m 'init' -r $testroot/repo $testroot/tree  \
> -		> $testroot/stdout
> +	gotadmin import -b newbranch -m 'init' -r $testroot/repo \
> +		$testroot/tree  > $testroot/stdout
>  	ret=$?
>  	test_done "$testroot" "$ret"
>  
> @@ -181,7 +181,7 @@ test_import_ignores() {
>  	mkdir $testroot/tree
>  	make_test_tree $testroot/tree
>  
> -	got import -I alpha -I '*lta*' -I '*silon' \
> +	gotadmin import -I alpha -I '*lta*' -I '*silon' \
>  		-m 'init' -r $testroot/repo $testroot/tree > $testroot/stdout
>  	ret=$?
>  	if [ $ret -ne 0 ]; then
> @@ -212,7 +212,8 @@ test_import_empty_dir() {
>  	mkdir -p $testroot/tree/empty $testroot/tree/notempty
>  	echo "alpha" > $testroot/tree/notempty/alpha
>  
> -	got import -m 'init' -r $testroot/repo $testroot/tree > $testroot/stdout
> +	gotadmin import -m 'init' -r $testroot/repo $testroot/tree \
> +		> $testroot/stdout
>  	ret=$?
>  	if [ $ret -ne 0 ]; then
>  		test_done "$testroot" "$ret"
> @@ -255,7 +256,7 @@ test_import_symlink() {
>  	echo 'this is file alpha' > $testroot/tree/alpha
>  	ln -s alpha $testroot/tree/alpha.link
>  
> -	got import -m 'init' -r $testroot/repo $testroot/tree \
> +	gotadmin import -m 'init' -r $testroot/repo $testroot/tree \
>  		> $testroot/stdout
>  	ret=$?
>  	if [ $ret -ne 0 ]; then
> blob - fc218eff521a465dc36ca25aad4973aa104fbb4c
> blob + 761b9de05dbc0b3614182f2090c3ab0910ee63dc
> --- regress/cmdline/log.sh
> +++ regress/cmdline/log.sh
> @@ -673,7 +673,8 @@ test_log_in_worktree_different_repo() {
>  	gotadmin init $testroot/other-repo
>  	mkdir -p $testroot/tree
>  	make_test_tree $testroot/tree
> -	got import -mm -b foo -r $testroot/other-repo $testroot/tree >/dev/null
> +	gotadmin import -mm -b foo -r $testroot/other-repo $testroot/tree \
> +		> /dev/null
>  	got checkout -b foo $testroot/other-repo $testroot/wt > /dev/null
>  	ret=$?
>  	if [ $ret -ne 0 ]; then
> blob - 0d03d0c83a725429e9a8d665b4e26dcbfe287e8c
> blob + e46030af3bd1a83b5c011e791d330e41b0254b70
> --- regress/cmdline/merge.sh
> +++ regress/cmdline/merge.sh
> @@ -1155,7 +1155,7 @@ test_merge_imported_branch() {
>  	echo "there should" > $testroot/tree/there/should
>  	echo "be lots of" > $testroot/tree/be/lots/of
>  	echo "files here" > $testroot/tree/files/here
> -	got import -r $testroot/repo -b files -m 'import files' \
> +	gotadmin import -r $testroot/repo -b files -m 'import files' \
>  		$testroot/tree > /dev/null
>  
>  	got checkout -b master $testroot/repo $testroot/wt > /dev/null
> 
> 

-- 

Tracey Emery