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

From:
Stefan Sperling <stsp@stsp.name>
Subject:
got import -> gotadmin import
To:
gameoftrees@openbsd.org
Date:
Wed, 6 Jul 2022 08:58:44 +0200

Download raw body.

Thread
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?

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