Commit Diff


commit - 78fb09675bee22e8ae24eb8bd9a86d8a04b17a7e
commit + 257add310e4b16ae43b467b91f66b773da39f470
blob - 19122e372a158d8a95216bb854924e9a25225a76
blob + fc2c1967ab5a100d7afd85ed536d8bec33d4c31f
--- got/got.1
+++ got/got.1
@@ -88,7 +88,9 @@ The
 command requires the
 .Ev GOT_AUTHOR
 environment variable to be set,
-unless Git's
+unless an author has been configured in
+.Xr got.conf 5
+or Git's
 .Dv user.name
 and
 .Dv user.email
@@ -186,8 +188,10 @@ More details about the pack file format are documented
 .Pp
 .Cm got clone
 creates a remote repository entry in the
+.Xr got.conf 5
+and
 .Pa config
-file of the cloned repository to store the
+files of the cloned repository to store the
 .Ar repository-url
 for future use by
 .Cm got fetch
@@ -234,8 +238,10 @@ This is useful if the cloned repository will not be us
 locally created commits.
 .Pp
 The repository's
+.Xr got.conf 5
+and
 .Pa config
-file will be set up with the
+files will be set up with the
 .Dq mirror
 option enabled, such that
 .Cm got fetch
@@ -310,6 +316,8 @@ is specified,
 .Dq origin
 will be used.
 The remote repository's URL is obtained from the corresponding entry in the
+.Xr got.conf 5
+or
 .Pa config
 file of the local repository, as created by
 .Cm got clone .
@@ -1228,7 +1236,9 @@ The
 command requires the
 .Ev GOT_AUTHOR
 environment variable to be set,
-unless Git's
+unless an author has been configured in
+.Xr got.conf 5
+or Git's
 .Dv user.name
 and
 .Dv user.email
@@ -1920,25 +1930,29 @@ attempts to reject
 .Ev GOT_AUTHOR
 environment variables with a missing email address.
 .Pp
-If present, Git's
+If present,
+configuration settings in
+.Xr got.conf 5 ,
+or Git's
 .Dv user.name
 and
 .Dv user.email
 configuration settings in the repository's
 .Pa .git/config
-file will override the value of
+file,
+will override the value of
 .Ev GOT_AUTHOR .
-However, the
+The
 .Dv user.name
 and
 .Dv user.email
 configuration settings contained in Git's global
 .Pa ~/.gitconfig
-configuration file will be used only if the
+configuration file will only be used if neither
+.Xr got.conf 5
+nor the
 .Ev GOT_AUTHOR
-environment variable is
-.Em not
-set.
+environment variable provide author information.
 .It Ev VISUAL , EDITOR
 The editor spawned by
 .Cm got commit ,
@@ -1951,6 +1965,16 @@ The default limit on the number of commits traversed b
 If set to zero, the limit is unbounded.
 This variable will be silently ignored if it is set to a non-numeric value.
 .El
+.Sh FILES
+.Bl -tag -width packed-refs -compact
+.It Pa got.conf
+Repository-wide configuration settings for
+.Nm .
+If present, this configuration file is located in the root directory
+of a Git repository and supersedes any relevant settings in Git's
+.Pa config
+file.
+.El
 .Sh EXIT STATUS
 .Ex -std got
 .Sh EXAMPLES
@@ -2257,7 +2281,8 @@ repository with
 .Sh SEE ALSO
 .Xr tog 1 ,
 .Xr git-repository 5 ,
-.Xr got-worktree 5
+.Xr got-worktree 5 ,
+.Xr got.conf 5
 .Sh AUTHORS
 .An Stefan Sperling Aq Mt stsp@openbsd.org
 .An Martin Pieuchot Aq Mt mpi@openbsd.org
blob - 2b4d757744e69bb5461dd333ec37c8351cdeaae0
blob + 45468e19e41e08dc67438153100b3e1883d44e32
--- got/got.c
+++ got/got.c
@@ -523,25 +523,30 @@ get_author(char **author, struct got_repository *repo)
 
 	*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");
+	got_author = got_repo_get_gotconfig_author(repo);
 	if (got_author == NULL) {
-		name = got_repo_get_global_gitconfig_author_name(repo);
-		email = got_repo_get_global_gitconfig_author_email(repo);
+		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;
 		}
-		/* TODO: Look up user in password database? */
-		return got_error(GOT_ERR_COMMIT_NO_AUTHOR);
+
+		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);
@@ -1037,9 +1042,9 @@ cmd_clone(int argc, char *argv[])
 	pid_t fetchpid = -1;
 	struct got_fetch_progress_arg fpa;
 	char *git_url = NULL;
-	char *gitconfig_path = NULL;
-	char *gitconfig = NULL;
-	FILE *gitconfig_file = NULL;
+	char *gitconfig_path = NULL, *gotconfig_path = NULL;
+	char *gitconfig = NULL, *gotconfig = NULL;
+	FILE *gitconfig_file = NULL, *gotconfig_file = NULL;
 	ssize_t n;
 	int verbosity = 0, fetch_all_branches = 0, mirror_references = 0;
 	int list_refs_only = 0;
@@ -1340,7 +1345,40 @@ cmd_clone(int argc, char *argv[])
 		}
 	}
 
-	/* Create a config file git-fetch(1) can understand. */
+	/* Create got.conf(5). */
+	gotconfig_path = got_repo_get_path_gotconfig(repo);
+	if (gotconfig_path == NULL) {
+		error = got_error_from_errno("got_repo_get_path_gotconfig");
+		goto done;
+	}
+	gotconfig_file = fopen(gotconfig_path, "a");
+	if (gotconfig_file == NULL) {
+		error = got_error_from_errno2("fopen", gotconfig_path);
+		goto done;
+	}
+	got_path_strip_trailing_slashes(server_path);
+	if (asprintf(&gotconfig,
+	    "remote \"%s\" {\n"
+	    "\tserver %s\n"
+	    "\tprotocol %s\n"
+	    "%s%s%s"
+	    "\trepository \"%s\"\n"
+	    "%s"
+	    "}\n",
+	    GOT_FETCH_DEFAULT_REMOTE_NAME, host, proto,
+	    port ? "\tport " : "", port ? port : "", port ? "\n" : "",
+	    server_path,
+	    mirror_references ? "\tmirror-references yes\n" : "") == -1) {
+		error = got_error_from_errno("asprintf");
+		goto done;
+	}
+	n = fwrite(gotconfig, 1, strlen(gotconfig), gotconfig_file);
+	if (n != strlen(gotconfig)) {
+		error = got_ferror(gotconfig_file, GOT_ERR_IO);
+		goto done;
+	}
+
+	/* Create a config file Git can understand. */
 	gitconfig_path = got_repo_get_path_gitconfig(repo);
 	if (gitconfig_path == NULL) {
 		error = got_error_from_errno("got_repo_get_path_gitconfig");
@@ -1413,6 +1451,8 @@ done:
 	}
 	if (fetchfd != -1 && close(fetchfd) == -1 && error == NULL)
 		error = got_error_from_errno("close");
+	if (gotconfig_file && fclose(gotconfig_file) == EOF && error == NULL)
+		error = got_error_from_errno("fclose");
 	if (gitconfig_file && fclose(gitconfig_file) == EOF && error == NULL)
 		error = got_error_from_errno("fclose");
 	if (repo)
@@ -1438,6 +1478,9 @@ done:
 	free(server_path);
 	free(repo_name);
 	free(default_destdir);
+	free(gotconfig);
+	free(gitconfig);
+	free(gotconfig_path);
 	free(gitconfig_path);
 	free(git_url);
 	return error;
@@ -1858,15 +1901,23 @@ cmd_fetch(int argc, char *argv[])
 	if (error)
 		goto done;
 
-	got_repo_get_gitconfig_remotes(&nremotes, &remotes, repo);
+	got_repo_get_gotconfig_remotes(&nremotes, &remotes, repo);
 	for (i = 0; i < nremotes; i++) {
 		remote = &remotes[i];
 		if (strcmp(remote->name, remote_name) == 0)
 			break;
 	}
 	if (i == nremotes) {
-		error = got_error_path(remote_name, GOT_ERR_NO_REMOTE);
-		goto done;
+		got_repo_get_gitconfig_remotes(&nremotes, &remotes, repo);
+		for (i = 0; i < nremotes; i++) {
+			remote = &remotes[i];
+			if (strcmp(remote->name, remote_name) == 0)
+				break;
+		}
+		if (i == nremotes) {
+			error = got_error_path(remote_name, GOT_ERR_NO_REMOTE);
+			goto done;
+		}
 	}
 
 	error = got_fetch_parse_uri(&proto, &host, &port, &server_path,
blob - /dev/null
blob + d6d11e44ab3797c4469bc5d41a882b8be638c18d (mode 644)
--- /dev/null
+++ got/got.conf.5
@@ -0,0 +1,156 @@
+.\"
+.\" Copyright (c) 2020 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.
+.\"
+.Dd $Mdocdate$
+.Dt GOT.CONF 5
+.Os
+.Sh NAME
+.Nm got.conf
+.Nd Game of Trees configuration file
+.Sh DESCRIPTION
+.Nm
+is the run-time configuration file for
+.Xr got 1 .
+.Pp
+The file format is line-based, with one configuration directive per line.
+Any lines beginning with a
+.Sq #
+are treated as comments and ignored.
+.Pp
+The available configuration directives are as follows:
+.Bl -tag -width Ds
+.It Ic author Dq Real Name <email address>
+Configure the author's name and email address for
+.Cm got commit
+and
+.Cm got import
+when operating on this repository.
+Author information specified here overrides the
+.Ev GOT_AUTHOR
+environment variable.
+.Pp
+Because
+.Xr git 1
+may fail to parse commits without an email address in author data,
+.Xr got 1
+attempts to reject author information with a missing email address.
+.It Ic remote Ar name Brq ...
+Define a remote repository.
+The specified
+.Ar name
+can be used to refer to the remote repository on the command line of
+.Cm got fetch .
+.Pp
+Information about this repository is declared in a block of options
+enclosed in curly brackets:
+.Bl -tag -width Ds
+.It Ic server Ar hostname
+Defines the hostname to use for contacting the remote repository's server.
+.It Ic repository Ar path
+Defines the path to the repository on the remote repository's server.
+.It Ic protocol Ar scheme
+Defines the protocol to use for communicating with the remote repository's
+server.
+.Pp
+The following protocol schemes are supported:
+.Bl -tag -width git+ssh
+.It git
+The Git protocol as implemented by the
+.Xr git-daemon 1
+server.
+Use of this protocol is discouraged since it supports neither authentication
+nor encryption.
+.It git+ssh
+The Git protocol wrapped in an authenticated and encrypted
+.Xr ssh 1
+tunnel.
+With this protocol the hostname may contain an embedded username for
+.Xr ssh 1
+to use:
+.Mt user@hostname
+.It ssh
+Short alias for git+ssh.
+.El
+.It Ic port Ar port
+Defines the port to use for connecting to the remote repository's server.
+The
+.Ar port
+can be specified by number or name.
+The port name to number mappings are found in the file
+.Pa /etc/services ;
+see
+.Xr services 5
+for details.
+If not specified, the default port of the specified
+.Cm protocol
+will be used.
+.It Ic mirror-references Ar yes | no
+This option controls the behaviour of
+.Cm got fetch
+when updating references.
+.Sy Enabling this option can lead to the loss of local commits.
+Maintaining custom changes in a mirror repository is therefore discouraged.
+.Pp
+If this option is not specified or set to
+.Ar no ,
+.Cm got fetch
+will map references of the remote repository into the local repository's
+.Dq refs/remotes/
+namespace.
+.Pp
+If this option is set to
+.Ar yes ,
+all branches in the
+.Dq refs/heads/
+namespace will be updated directly to match the corresponding branches in
+the remote repository.
+.El
+.Sh EXAMPLES
+Configure author information:
+.Bd -literal -offset indent
+author "Flan Hacker <flan_hacker@openbsd.org>"
+.Ed
+.Pp
+Remote repository specification for the Game of Trees repository:
+.Bd -literal -offset indent
+remote "origin" {
+	server git.gameoftrees.org
+	protocol git
+	repository got
+}
+.Ed
+.Pp
+Mirror the OpenBSD src repository from Github:
+.Bd -literal -offset indent
+remote "origin" {
+	repository "openbsd/src"
+	server git@github.com
+	protocol git+ssh
+	mirror-references yes
+}
+.Ed
+.Sh FILES
+.Bl -tag -width Ds -compact
+.It Pa got.conf
+If present, the
+.Nm
+configuration file is located in the root directory of a Git repository
+and supersedes any relevant settings in Git's
+.Pa config
+file.
+.El
+.Sh SEE ALSO
+.Xr got 1 ,
+.Xr git-repository 5
blob - 93dc3d73083ef892787d94815eeedb416e7fc4e3
blob + 93c6e03f94ed0176787d92976b2a68085ae10de3
--- gotweb/parse.y
+++ gotweb/parse.y
@@ -234,7 +234,7 @@ yyerror(const char *fmt, ...)
 		gerror = got_error_from_errno("asprintf");
 		return(0);
 	}
-	gerror = got_error_msg(GOT_ERR_PARSE_Y_YY, strdup(err));
+	gerror = got_error_msg(GOT_ERR_PARSE_CONFIG, strdup(err));
 	free(msg);
 	free(err);
 	return(0);
blob - b3805d39c06efa63932243754b6f446fa0218eda
blob + 0f3e07a914451fb4ee60661753010efd60284036
--- include/got_error.h
+++ include/got_error.h
@@ -141,7 +141,7 @@
 #define GOT_ERR_FETCH_NO_BRANCH	124
 #define GOT_ERR_FETCH_BAD_REF	125
 #define GOT_ERR_TREE_ENTRY_TYPE	126
-#define GOT_ERR_PARSE_Y_YY	127
+#define GOT_ERR_PARSE_CONFIG	127
 #define GOT_ERR_NO_CONFIG_FILE	128
 #define GOT_ERR_BAD_SYMLINK	129
 
@@ -291,7 +291,7 @@ static const struct got_error {
 	{ GOT_ERR_FETCH_NO_BRANCH, "could not find any branches to fetch" },
 	{ GOT_ERR_FETCH_BAD_REF, "reference cannot be fetched" },
 	{ GOT_ERR_TREE_ENTRY_TYPE, "unexpected tree entry type" },
-	{ GOT_ERR_PARSE_Y_YY, "yyerror error" },
+	{ GOT_ERR_PARSE_CONFIG, "configuration file syntax error" },
 	{ GOT_ERR_NO_CONFIG_FILE, "configuration file doesn't exit" },
 	{ GOT_ERR_BAD_SYMLINK, "symbolic link points outside of paths under "
 	    "version control" },
blob - 1e0ca3b4ba3d818cceefe6c470559274e954788c
blob + bc9bddc1aebed32b0412d1f87f93b3ec0b753271
--- include/got_repository.h
+++ include/got_repository.h
@@ -59,10 +59,17 @@ struct got_remote_repo {
 	int mirror_references;
 };
 
+/* Obtain the commit author if parsed from got.conf, else NULL. */
+const char *got_repo_get_gotconfig_author(struct got_repository *);
+
 /* Obtain the list of remote repositories parsed from gitconfig. */ 
 void got_repo_get_gitconfig_remotes(int *, struct got_remote_repo **,
     struct got_repository *);
 
+/* Obtain the list of remote repositories parsed from got.conf. */ 
+void got_repo_get_gotconfig_remotes(int *, struct got_remote_repo **,
+    struct got_repository *);
+
 /*
  * Obtain paths to various directories within a repository.
  * The caller must dispose of a path with free(3).
@@ -72,6 +79,7 @@ char *got_repo_get_path_objects_pack(struct got_reposi
 char *got_repo_get_path_refs(struct got_repository *);
 char *got_repo_get_path_packed_refs(struct got_repository *);
 char *got_repo_get_path_gitconfig(struct got_repository *);
+char *got_repo_get_path_gotconfig(struct got_repository *);
 
 struct got_reference;
 
blob - 950723fdd3cab55b86ef0eb87c5739432be13b7c
blob + 51acfb2fde45abd2e9073411ad36ae9a24fed30e
--- lib/got_lib_privsep.h
+++ lib/got_lib_privsep.h
@@ -43,6 +43,7 @@
 #define GOT_PROG_READ_TAG	got-read-tag
 #define GOT_PROG_READ_PACK	got-read-pack
 #define GOT_PROG_READ_GITCONFIG	got-read-gitconfig
+#define GOT_PROG_READ_GOTCONFIG	got-read-gotconfig
 #define GOT_PROG_FETCH_PACK	got-fetch-pack
 #define GOT_PROG_INDEX_PACK	got-index-pack
 #define GOT_PROG_SEND_PACK	got-send-pack
@@ -65,6 +66,8 @@
 	GOT_STRINGVAL(GOT_LIBEXECDIR) "/" GOT_STRINGVAL(GOT_PROG_READ_PACK)
 #define GOT_PATH_PROG_READ_GITCONFIG \
 	GOT_STRINGVAL(GOT_LIBEXECDIR) "/" GOT_STRINGVAL(GOT_PROG_READ_GITCONFIG)
+#define GOT_PATH_PROG_READ_GOTCONFIG \
+	GOT_STRINGVAL(GOT_LIBEXECDIR) "/" GOT_STRINGVAL(GOT_PROG_READ_GOTCONFIG)
 #define GOT_PATH_PROG_FETCH_PACK \
 	GOT_STRINGVAL(GOT_LIBEXECDIR) "/" GOT_STRINGVAL(GOT_PROG_FETCH_PACK)
 #define GOT_PATH_PROG_SEND_PACK \
@@ -147,6 +150,15 @@ enum got_imsg_type {
 	GOT_IMSG_GITCONFIG_REMOTE,
 	GOT_IMSG_GITCONFIG_OWNER_REQUEST,
 	GOT_IMSG_GITCONFIG_OWNER,
+
+	/* Messages related to gotconfig files. */
+	GOT_IMSG_GOTCONFIG_PARSE_REQUEST,
+	GOT_IMSG_GOTCONFIG_AUTHOR_REQUEST,
+	GOT_IMSG_GOTCONFIG_REMOTES_REQUEST,
+	GOT_IMSG_GOTCONFIG_INT_VAL,
+	GOT_IMSG_GOTCONFIG_STR_VAL,
+	GOT_IMSG_GOTCONFIG_REMOTES,
+	GOT_IMSG_GOTCONFIG_REMOTE,
 };
 
 /* Structure for GOT_IMSG_ERROR. */
@@ -457,6 +469,16 @@ const struct got_error *got_privsep_recv_gitconfig_str
     struct imsgbuf *);
 const struct got_error *got_privsep_recv_gitconfig_int(int *, struct imsgbuf *);
 const struct got_error *got_privsep_recv_gitconfig_remotes(
+    struct got_remote_repo **, int *, struct imsgbuf *);
+
+const struct got_error *got_privsep_send_gotconfig_parse_req(struct imsgbuf *,
+    int);
+const struct got_error *got_privsep_send_gotconfig_author_req(struct imsgbuf *);
+const struct got_error *got_privsep_send_gotconfig_remotes_req(
+    struct imsgbuf *);
+const struct got_error *got_privsep_recv_gotconfig_str(char **,
+    struct imsgbuf *);
+const struct got_error *got_privsep_recv_gotconfig_remotes(
     struct got_remote_repo **, int *, struct imsgbuf *);
 
 const struct got_error *got_privsep_send_commit_traversal_request(
blob - df440da3c28c0038eb1d0ecb1cf3af9501734493
blob + 53390eff4e10f3f105c870ac188b9a0db1df2416
--- lib/got_lib_repository.h
+++ lib/got_lib_repository.h
@@ -21,6 +21,7 @@
 #define GOT_REFS_DIR		"refs"
 #define GOT_HEAD_FILE		"HEAD"
 #define GOT_GITCONFIG		"config"
+#define GOT_GOTCONFIG		"got.conf"
 
 /* Other files and directories inside the git directory. */
 #define GOT_FETCH_HEAD_FILE	"FETCH_HEAD"
@@ -64,6 +65,11 @@ struct got_repository {
 	int ngitconfig_remotes;
 	struct got_remote_repo *gitconfig_remotes;
 	char *gitconfig_owner;
+
+	/* Settings read from got.conf. */
+	char *gotconfig_author;
+	int ngotconfig_remotes;
+	struct got_remote_repo *gotconfig_remotes;
 };
 
 const struct got_error*got_repo_cache_object(struct got_repository *,
blob - 5c21a195ee0e8cf21767c0bf8ec2efb2c841e1d1
blob + 63a8b0da31d4514f9839334c64e7108fa41ab78b
--- lib/privsep.c
+++ lib/privsep.c
@@ -1885,9 +1885,211 @@ got_privsep_recv_gitconfig_remotes(struct got_remote_r
 		*nremotes = 0;
 	}
 	return err;
+}
+
+const struct got_error *
+got_privsep_send_gotconfig_parse_req(struct imsgbuf *ibuf, int fd)
+{
+	const struct got_error *err = NULL;
+
+	if (imsg_compose(ibuf, GOT_IMSG_GOTCONFIG_PARSE_REQUEST, 0, 0, fd,
+	    NULL, 0) == -1) {
+		err = got_error_from_errno("imsg_compose "
+		    "GOTCONFIG_PARSE_REQUEST");
+		close(fd);
+		return err;
+	}
+
+	return flush_imsg(ibuf);
+}
+
+const struct got_error *
+got_privsep_send_gotconfig_author_req(struct imsgbuf *ibuf)
+{
+	if (imsg_compose(ibuf,
+	    GOT_IMSG_GOTCONFIG_AUTHOR_REQUEST, 0, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("imsg_compose "
+		    "GOTCONFIG_AUTHOR_REQUEST");
+
+	return flush_imsg(ibuf);
 }
 
 const struct got_error *
+got_privsep_send_gotconfig_remotes_req(struct imsgbuf *ibuf)
+{
+	if (imsg_compose(ibuf,
+	    GOT_IMSG_GOTCONFIG_REMOTES_REQUEST, 0, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("imsg_compose "
+		    "GOTCONFIG_REMOTE_REQUEST");
+
+	return flush_imsg(ibuf);
+}
+
+const struct got_error *
+got_privsep_recv_gotconfig_str(char **str, struct imsgbuf *ibuf)
+{
+	const struct got_error *err = NULL;
+	struct imsg imsg;
+	size_t datalen;
+	const size_t min_datalen = 0;
+
+	*str = NULL;
+
+	err = got_privsep_recv_imsg(&imsg, ibuf, min_datalen);
+	if (err)
+		return err;
+	datalen = imsg.hdr.len - IMSG_HEADER_SIZE;
+
+	switch (imsg.hdr.type) {
+	case GOT_IMSG_ERROR:
+		if (datalen < sizeof(struct got_imsg_error)) {
+			err = got_error(GOT_ERR_PRIVSEP_LEN);
+			break;
+		}
+		err = recv_imsg_error(&imsg, datalen);
+		break;
+	case GOT_IMSG_GOTCONFIG_STR_VAL:
+		if (datalen == 0)
+			break;
+		*str = malloc(datalen);
+		if (*str == NULL) {
+			err = got_error_from_errno("malloc");
+			break;
+		}
+		if (strlcpy(*str, imsg.data, datalen) >= datalen)
+			err = got_error(GOT_ERR_NO_SPACE);
+		break;
+	default:
+		err = got_error(GOT_ERR_PRIVSEP_MSG);
+		break;
+	}
+
+	imsg_free(&imsg);
+	return err;
+}
+
+const struct got_error *
+got_privsep_recv_gotconfig_remotes(struct got_remote_repo **remotes,
+    int *nremotes, struct imsgbuf *ibuf)
+{
+	const struct got_error *err = NULL;
+	struct imsg imsg;
+	size_t datalen;
+	struct got_imsg_remotes iremotes;
+	struct got_imsg_remote iremote;
+	const size_t min_datalen =
+	    MIN(sizeof(struct got_imsg_error), sizeof(iremotes));
+
+	*remotes = NULL;
+	*nremotes = 0;
+	iremotes.nremotes = 0;
+
+	err = got_privsep_recv_imsg(&imsg, ibuf, min_datalen);
+	if (err)
+		return err;
+	datalen = imsg.hdr.len - IMSG_HEADER_SIZE;
+
+	switch (imsg.hdr.type) {
+	case GOT_IMSG_ERROR:
+		if (datalen < sizeof(struct got_imsg_error)) {
+			err = got_error(GOT_ERR_PRIVSEP_LEN);
+			break;
+		}
+		err = recv_imsg_error(&imsg, datalen);
+		break;
+	case GOT_IMSG_GOTCONFIG_REMOTES:
+		if (datalen != sizeof(iremotes)) {
+			err = got_error(GOT_ERR_PRIVSEP_LEN);
+			break;
+		}
+		memcpy(&iremotes, imsg.data, sizeof(iremotes));
+		if (iremotes.nremotes == 0) {
+			imsg_free(&imsg);
+			return NULL;
+		}
+		break;
+	default:
+		imsg_free(&imsg);
+		return got_error(GOT_ERR_PRIVSEP_MSG);
+	}
+
+	imsg_free(&imsg);
+
+	*remotes = recallocarray(NULL, 0, iremotes.nremotes, sizeof(**remotes));
+	if (*remotes == NULL)
+		return got_error_from_errno("recallocarray");
+
+	while (*nremotes < iremotes.nremotes) {
+		struct got_remote_repo *remote;
+		const size_t min_datalen =
+		    MIN(sizeof(struct got_imsg_error), sizeof(iremote));
+
+		err = got_privsep_recv_imsg(&imsg, ibuf, min_datalen);
+		if (err)
+			break;
+		datalen = imsg.hdr.len - IMSG_HEADER_SIZE;
+
+		switch (imsg.hdr.type) {
+		case GOT_IMSG_ERROR:
+			if (datalen < sizeof(struct got_imsg_error)) {
+				err = got_error(GOT_ERR_PRIVSEP_LEN);
+				break;
+			}
+			err = recv_imsg_error(&imsg, datalen);
+			break;
+		case GOT_IMSG_GOTCONFIG_REMOTE:
+			remote = &(*remotes)[*nremotes];
+			if (datalen < sizeof(iremote)) {
+				err = got_error(GOT_ERR_PRIVSEP_LEN);
+				break;
+			}
+			memcpy(&iremote, imsg.data, sizeof(iremote));
+			if (iremote.name_len == 0 || iremote.url_len == 0 ||
+			    (sizeof(iremote) + iremote.name_len +
+			    iremote.url_len) > datalen) {
+				err = got_error(GOT_ERR_PRIVSEP_LEN);
+				break;
+			}
+			remote->name = strndup(imsg.data + sizeof(iremote),
+			    iremote.name_len);
+			if (remote->name == NULL) {
+				err = got_error_from_errno("strndup");
+				break;
+			}
+			remote->url = strndup(imsg.data + sizeof(iremote) +
+			    iremote.name_len, iremote.url_len);
+			if (remote->url == NULL) {
+				err = got_error_from_errno("strndup");
+				free(remote->name);
+				break;
+			}
+			remote->mirror_references = iremote.mirror_references;
+			(*nremotes)++;
+			break;
+		default:
+			err = got_error(GOT_ERR_PRIVSEP_MSG);
+			break;
+		}
+
+		imsg_free(&imsg);
+		if (err)
+			break;
+	}
+
+	if (err) {
+		int i;
+		for (i = 0; i < *nremotes; i++) {
+			free((*remotes)[i].name);
+			free((*remotes)[i].url);
+		}
+		free(*remotes);
+		*remotes = NULL;
+		*nremotes = 0;
+	}
+	return err;
+}
+
+const struct got_error *
 got_privsep_send_commit_traversal_request(struct imsgbuf *ibuf,
      struct got_object_id *id, int idx, const char *path)
 {
@@ -2009,6 +2211,7 @@ got_privsep_unveil_exec_helpers(void)
 	    GOT_PATH_PROG_READ_BLOB,
 	    GOT_PATH_PROG_READ_TAG,
 	    GOT_PATH_PROG_READ_GITCONFIG,
+	    GOT_PATH_PROG_READ_GOTCONFIG,
 	    GOT_PATH_PROG_FETCH_PACK,
 	    GOT_PATH_PROG_INDEX_PACK,
 	};
blob - c2ee66c55abf30b586e88619a4a81d4be0c443bf
blob + b120196a4d400ea2d257c39efc48145d0508855e
--- lib/repository.c
+++ lib/repository.c
@@ -161,6 +161,12 @@ got_repo_get_path_gitconfig(struct got_repository *rep
 	return get_path_git_child(repo, GOT_GITCONFIG);
 }
 
+char *
+got_repo_get_path_gotconfig(struct got_repository *repo)
+{
+	return get_path_git_child(repo, GOT_GOTCONFIG);
+}
+
 void
 got_repo_get_gitconfig_remotes(int *nremotes, struct got_remote_repo **remotes,
     struct got_repository *repo)
@@ -169,6 +175,20 @@ got_repo_get_gitconfig_remotes(int *nremotes, struct g
 	*remotes = repo->gitconfig_remotes;
 }
 
+const char *
+got_repo_get_gotconfig_author(struct got_repository *repo)
+{
+	return repo->gotconfig_author;
+}
+
+void
+got_repo_get_gotconfig_remotes(int *nremotes, struct got_remote_repo **remotes,
+    struct got_repository *repo)
+{
+	*nremotes = repo->ngotconfig_remotes;
+	*remotes = repo->gotconfig_remotes;
+}
+
 static int
 is_git_repo(struct got_repository *repo)
 {
@@ -515,6 +535,124 @@ read_gitconfig(struct got_repository *repo, const char
 		goto done;
 done:
 	free(repo_gitconfig_path);
+	return err;
+}
+
+static const struct got_error *
+parse_gotconfig_file(char **author,
+    struct got_remote_repo **remotes, int *nremotes,
+    const char *gotconfig_path)
+{
+	const struct got_error *err = NULL, *child_err = NULL;
+	int fd = -1;
+	int imsg_fds[2] = { -1, -1 };
+	pid_t pid;
+	struct imsgbuf *ibuf;
+
+	if (author)
+		*author = NULL;
+	if (remotes)
+		*remotes = NULL;
+	if (nremotes)
+		*nremotes = 0;
+
+	fd = open(gotconfig_path, O_RDONLY);
+	if (fd == -1) {
+		if (errno == ENOENT)
+			return NULL;
+		return got_error_from_errno2("open", gotconfig_path);
+	}
+
+	ibuf = calloc(1, sizeof(*ibuf));
+	if (ibuf == NULL) {
+		err = got_error_from_errno("calloc");
+		goto done;
+	}
+
+	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, imsg_fds) == -1) {
+		err = got_error_from_errno("socketpair");
+		goto done;
+	}
+
+	pid = fork();
+	if (pid == -1) {
+		err = got_error_from_errno("fork");
+		goto done;
+	} else if (pid == 0) {
+		got_privsep_exec_child(imsg_fds, GOT_PATH_PROG_READ_GOTCONFIG,
+		    gotconfig_path);
+		/* not reached */
+	}
+
+	if (close(imsg_fds[1]) == -1) {
+		err = got_error_from_errno("close");
+		goto done;
+	}
+	imsg_fds[1] = -1;
+	imsg_init(ibuf, imsg_fds[0]);
+
+	err = got_privsep_send_gotconfig_parse_req(ibuf, fd);
+	if (err)
+		goto done;
+	fd = -1;
+
+	if (author) {
+		err = got_privsep_send_gotconfig_author_req(ibuf);
+		if (err)
+			goto done;
+
+		err = got_privsep_recv_gotconfig_str(author, ibuf);
+		if (err)
+			goto done;
+	}
+
+	if (remotes && nremotes) {
+		err = got_privsep_send_gotconfig_remotes_req(ibuf);
+		if (err)
+			goto done;
+
+		err = got_privsep_recv_gotconfig_remotes(remotes,
+		    nremotes, ibuf);
+		if (err)
+			goto done;
+	}
+
+	imsg_clear(ibuf);
+	err = got_privsep_send_stop(imsg_fds[0]);
+	child_err = got_privsep_wait_for_child(pid);
+	if (child_err && err == NULL)
+		err = child_err;
+done:
+	if (imsg_fds[0] != -1 && close(imsg_fds[0]) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (imsg_fds[1] != -1 && close(imsg_fds[1]) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (fd != -1 && close(fd) == -1 && err == NULL)
+		err = got_error_from_errno2("close", gotconfig_path);
+	if (err) {
+		if (author) {
+			free(*author);
+			*author = NULL;
+		}
+	}
+	free(ibuf);
+	return err;
+}
+
+static const struct got_error *
+read_gotconfig(struct got_repository *repo)
+{
+	const struct got_error *err = NULL;
+	char *gotconfig_path;
+
+	gotconfig_path = got_repo_get_path_gotconfig(repo);
+	if (gotconfig_path == NULL)
+		return got_error_from_errno("got_repo_get_path_gotconfig");
+
+	err = parse_gotconfig_file(&repo->gotconfig_author,
+	    &repo->gotconfig_remotes, &repo->ngotconfig_remotes,
+	    gotconfig_path);
+	free(gotconfig_path);
 	return err;
 }
 
@@ -591,6 +729,10 @@ got_repo_open(struct got_repository **repop, const cha
 		}
 	} while (path);
 
+	err = read_gotconfig(repo);
+	if (err)
+		goto done;
+
 	err = read_gitconfig(repo, global_gitconfig_path);
 	if (err)
 		goto done;
@@ -646,6 +788,12 @@ got_repo_close(struct got_repository *repo)
 			err = got_error_from_errno("close");
 	}
 
+	free(repo->gotconfig_author);
+	for (i = 0; i < repo->ngotconfig_remotes; i++) {
+		free(repo->gotconfig_remotes[i].name);
+		free(repo->gotconfig_remotes[i].url);
+	}
+	free(repo->gotconfig_remotes);
 	free(repo->gitconfig_author_name);
 	free(repo->gitconfig_author_email);
 	for (i = 0; i < repo->ngitconfig_remotes; i++) {
blob - 41c5a86903979f5cc3495303157b082ea384ee7f
blob + 1e55c9808beb7f0984445d4aae36eaddd0ceb5d4
--- libexec/Makefile
+++ libexec/Makefile
@@ -1,5 +1,5 @@
 SUBDIR = got-read-blob got-read-commit got-read-object got-read-tree \
 	got-read-tag got-fetch-pack got-index-pack got-read-pack \
-	got-read-gitconfig
+	got-read-gitconfig got-read-gotconfig
 
 .include <bsd.subdir.mk>
blob - /dev/null
blob + a683cf2ee1b6cf0753a4a7f2b006bfc29f853a8c (mode 644)
--- /dev/null
+++ libexec/got-read-gotconfig/Makefile
@@ -0,0 +1,13 @@
+.PATH:${.CURDIR}/../../lib
+
+.include "../../got-version.mk"
+
+PROG=		got-read-gotconfig
+SRCS=		got-read-gotconfig.c error.c inflate.c object_parse.c \
+		path.c privsep.c sha1.c parse.y
+
+CPPFLAGS = -I${.CURDIR}/../../include -I${.CURDIR}/../../lib -I${.CURDIR}
+LDADD = -lutil -lz
+DPADD = ${LIBZ} ${LIBUTIL}
+
+.include <bsd.prog.mk>
blob - /dev/null
blob + e3bcc200c6b5f1d8417e2926319744311bb40275 (mode 644)
--- /dev/null
+++ libexec/got-read-gotconfig/got-read-gotconfig.c
@@ -0,0 +1,358 @@
+/*
+ * Copyright (c) 2020 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/types.h>
+#include <sys/queue.h>
+#include <sys/uio.h>
+#include <sys/time.h>
+#include <sys/syslimits.h>
+
+#include <stdint.h>
+#include <imsg.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sha1.h>
+#include <zlib.h>
+
+#include "got_error.h"
+#include "got_object.h"
+#include "got_repository.h"
+
+#include "got_lib_delta.h"
+#include "got_lib_object.h"
+#include "got_lib_privsep.h"
+
+#include "gotconfig.h"
+
+/* parse.y */
+static volatile sig_atomic_t sigint_received;
+
+static void
+catch_sigint(int signo)
+{
+	sigint_received = 1;
+}
+
+static const struct got_error *
+make_repo_url(char **url, struct gotconfig_remote_repo *repo)
+{
+	const struct got_error *err = NULL;
+	char *s = NULL, *p = NULL;
+
+	*url = NULL;
+
+	if (asprintf(&s, "%s://", repo->protocol) == -1)
+		return got_error_from_errno("asprintf");
+
+	if (repo->server) {
+		p = s;
+		s = NULL;
+		if (asprintf(&s, "%s%s", p, repo->server) == -1) {
+			err = got_error_from_errno("asprintf");
+			goto done;
+		}
+		free(p);
+		p = NULL;
+	}
+
+	if (repo->port) {
+		p = s;
+		s = NULL;
+		if (asprintf(&s, "%s:%d", p, repo->port) == -1) {
+			err = got_error_from_errno("asprintf");
+			goto done;
+		}
+		free(p);
+		p = NULL;
+	}
+
+	if (repo->repository) {
+		p = s;
+		s = NULL;
+		if (asprintf(&s, "%s/%s", p, repo->repository) == -1) {
+			err = got_error_from_errno("asprintf");
+			goto done;
+		}
+		free(p);
+		p = NULL;
+	}
+done:
+	if (err) {
+		free(s);
+		free(p);
+	} else
+		*url = s;
+	return err;
+}
+
+static const struct got_error *
+send_gotconfig_str(struct imsgbuf *ibuf, const char *value)
+{
+	size_t len = value ? strlen(value) + 1 : 0;
+
+	if (imsg_compose(ibuf, GOT_IMSG_GOTCONFIG_STR_VAL, 0, 0, -1,
+	    value, len) == -1)
+		return got_error_from_errno("imsg_compose GOTCONFIG_STR_VAL");
+
+	return got_privsep_flush_imsg(ibuf);
+}
+
+static const struct got_error *
+send_gotconfig_remotes(struct imsgbuf *ibuf,
+    struct gotconfig_remote_repo_list *remotes, int nremotes)
+{
+	const struct got_error *err = NULL;
+	struct got_imsg_remotes iremotes;
+	struct gotconfig_remote_repo *repo;
+	char *url = NULL;
+
+	iremotes.nremotes = nremotes;
+	if (imsg_compose(ibuf, GOT_IMSG_GOTCONFIG_REMOTES, 0, 0, -1,
+	    &iremotes, sizeof(iremotes)) == -1)
+		return got_error_from_errno("imsg_compose GOTCONFIG_REMOTES");
+
+	err = got_privsep_flush_imsg(ibuf);
+	imsg_clear(ibuf);
+	if (err)
+		return err;
+
+	TAILQ_FOREACH(repo, remotes, entry) {
+		struct got_imsg_remote iremote;
+		size_t len = sizeof(iremote);
+		struct ibuf *wbuf;
+
+		iremote.mirror_references = repo->mirror_references;
+
+		iremote.name_len = strlen(repo->name);
+		len += iremote.name_len;
+
+		err = make_repo_url(&url, repo);
+		if (err)
+			break;
+		iremote.url_len = strlen(url);
+		len += iremote.url_len;
+
+		wbuf = imsg_create(ibuf, GOT_IMSG_GOTCONFIG_REMOTE, 0, 0, len);
+		if (wbuf == NULL) {
+			err = got_error_from_errno(
+			    "imsg_create GOTCONFIG_REMOTE");
+			break;
+		}
+
+		if (imsg_add(wbuf, &iremote, sizeof(iremote)) == -1) {
+			err = got_error_from_errno(
+			    "imsg_add GOTCONFIG_REMOTE");
+			ibuf_free(wbuf);
+			break;
+		}
+
+		if (imsg_add(wbuf, repo->name, iremote.name_len) == -1) {
+			err = got_error_from_errno(
+			    "imsg_add GOTCONFIG_REMOTE");
+			ibuf_free(wbuf);
+			break;
+		}
+		if (imsg_add(wbuf, url, iremote.url_len) == -1) {
+			err = got_error_from_errno(
+			    "imsg_add GOTCONFIG_REMOTE");
+			ibuf_free(wbuf);
+			break;
+		}
+
+		wbuf->fd = -1;
+		imsg_close(ibuf, wbuf);
+		err = got_privsep_flush_imsg(ibuf);
+		if (err)
+			break;
+
+		free(url);
+		url = NULL;
+	}
+
+	free(url);
+	return err;
+}
+
+static const struct got_error *
+validate_config(struct gotconfig *gotconfig)
+{
+	struct gotconfig_remote_repo *repo, *repo2;
+	static char msg[512];
+
+	TAILQ_FOREACH(repo, &gotconfig->remotes, entry) {
+		if (repo->name == NULL) {
+			return got_error_msg(GOT_ERR_PARSE_CONFIG,
+			    "name required for remote repository");
+		}
+
+		TAILQ_FOREACH(repo2, &gotconfig->remotes, entry) {
+			if (repo == repo2 ||
+			    strcmp(repo->name, repo2->name) != 0)
+				continue;
+			snprintf(msg, sizeof(msg),
+			    "duplicate remote repository name '%s'",
+			    repo->name);
+			return got_error_msg(GOT_ERR_PARSE_CONFIG, msg);
+		}
+
+		if (repo->server == NULL) {
+			snprintf(msg, sizeof(msg),
+			    "server required for remote repository \"%s\"",
+			    repo->name);
+			return got_error_msg(GOT_ERR_PARSE_CONFIG, msg);
+		}
+
+		if (repo->protocol == NULL) {
+			snprintf(msg, sizeof(msg),
+			    "protocol required for remote repository \"%s\"",
+			    repo->name);
+			return got_error_msg(GOT_ERR_PARSE_CONFIG, msg);
+		}
+		if (strcmp(repo->protocol, "ssh") != 0 &&
+		    strcmp(repo->protocol, "git+ssh") != 0 &&
+		    strcmp(repo->protocol, "git") != 0) {
+			snprintf(msg, sizeof(msg),"unknown protocol \"%s\" "
+			    "for remote repository \"%s\"", repo->protocol,
+			    repo->name);
+			return got_error_msg(GOT_ERR_PARSE_CONFIG, msg);
+		}
+
+		if (repo->repository == NULL) {
+			snprintf(msg, sizeof(msg),
+			    "repository path required for remote "
+			    "repository \"%s\"", repo->name);
+			return got_error_msg(GOT_ERR_PARSE_CONFIG, msg);
+		}
+	}
+
+	return NULL;
+}
+
+int
+main(int argc, char *argv[])
+{
+	const struct got_error *err = NULL;
+	struct imsgbuf ibuf;
+	struct gotconfig *gotconfig;
+	size_t datalen;
+	const char *filename = "got.conf";
+#if 0
+	static int attached;
+
+	while (!attached)
+		sleep(1);
+#endif
+	signal(SIGINT, catch_sigint);
+
+	imsg_init(&ibuf, GOT_IMSG_FD_CHILD);
+
+#ifndef PROFILE
+	/* revoke access to most system calls */
+	if (pledge("stdio recvfd", NULL) == -1) {
+		err = got_error_from_errno("pledge");
+		got_privsep_send_error(&ibuf, err);
+		return 1;
+	}
+#endif
+
+	if (argc > 1)
+		filename = argv[1];
+
+	for (;;) {
+		struct imsg imsg;
+
+		memset(&imsg, 0, sizeof(imsg));
+		imsg.fd = -1;
+
+		if (sigint_received) {
+			err = got_error(GOT_ERR_CANCELLED);
+			break;
+		}
+
+		err = got_privsep_recv_imsg(&imsg, &ibuf, 0);
+		if (err) {
+			if (err->code == GOT_ERR_PRIVSEP_PIPE)
+				err = NULL;
+			break;
+		}
+
+		if (imsg.hdr.type == GOT_IMSG_STOP)
+			break;
+
+		switch (imsg.hdr.type) {
+		case GOT_IMSG_GOTCONFIG_PARSE_REQUEST:
+			datalen = imsg.hdr.len - IMSG_HEADER_SIZE;
+			if (datalen != 0) {
+				err = got_error(GOT_ERR_PRIVSEP_LEN);
+				break;
+			}
+			if (imsg.fd == -1){
+				err = got_error(GOT_ERR_PRIVSEP_NO_FD);
+				break;
+			}
+
+			if (gotconfig)
+				gotconfig_free(gotconfig);
+			err = gotconfig_parse(&gotconfig, filename, &imsg.fd);
+			if (err)
+				break;
+			err = validate_config(gotconfig);
+			break;
+		case GOT_IMSG_GOTCONFIG_AUTHOR_REQUEST:
+			if (gotconfig == NULL) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = send_gotconfig_str(&ibuf,
+			    gotconfig->author ?  gotconfig->author : "");
+			break;
+		case GOT_IMSG_GOTCONFIG_REMOTES_REQUEST:
+			if (gotconfig == NULL) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = send_gotconfig_remotes(&ibuf,
+			    &gotconfig->remotes, gotconfig->nremotes);
+			break;
+		default:
+			err = got_error(GOT_ERR_PRIVSEP_MSG);
+			break;
+		}
+
+		if (imsg.fd != -1) {
+			if (close(imsg.fd) == -1 && err == NULL)
+				err = got_error_from_errno("close");
+		}
+
+		imsg_free(&imsg);
+		if (err)
+			break;
+	}
+
+	imsg_clear(&ibuf);
+	if (err) {
+		if (!sigint_received && err->code != GOT_ERR_PRIVSEP_PIPE) {
+			fprintf(stderr, "%s: %s\n", getprogname(), err->msg);
+			got_privsep_send_error(&ibuf, err);
+		}
+	}
+	if (close(GOT_IMSG_FD_CHILD) != 0 && err == NULL)
+		err = got_error_from_errno("close");
+	return err ? 1 : 0;
+}
blob - /dev/null
blob + ab55bd31f17ddbcdf0a483b21903f7e00a588c20 (mode 644)
--- /dev/null
+++ libexec/got-read-gotconfig/gotconfig.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2020 Tracey Emery <tracey@openbsd.org>
+ * Copyright (c) 2020 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.
+ */
+
+struct gotconfig_remote_repo {
+	TAILQ_ENTRY(gotconfig_remote_repo) entry;
+	char	*name;
+	char	*repository;
+	char	*server;
+	char	*protocol;
+	int	port;
+	int	mirror_references;
+};
+TAILQ_HEAD(gotconfig_remote_repo_list, gotconfig_remote_repo);
+
+struct gotconfig {
+	char	*author;
+	struct gotconfig_remote_repo_list remotes;
+	int nremotes;
+};
+
+/*
+ * Parse individual gotconfig repository files
+ */
+const struct got_error *gotconfig_parse(struct gotconfig **, const char *,
+    int *);
+void gotconfig_free(struct gotconfig *);
blob - /dev/null
blob + 4bba6446d36f1aeb5246780345b255b6a26153e7 (mode 644)
--- /dev/null
+++ libexec/got-read-gotconfig/parse.y
@@ -0,0 +1,762 @@
+/*
+ * Copyright (c) 2020 Tracey Emery <tracey@openbsd.org>
+ * Copyright (c) 2020 Stefan Sperling <stsp@openbsd.org>
+ * Copyright (c) 2004, 2005 Esben Norby <norby@openbsd.org>
+ * Copyright (c) 2004 Ryan McBride <mcbride@openbsd.org>
+ * Copyright (c) 2002, 2003, 2004 Henning Brauer <henning@openbsd.org>
+ * Copyright (c) 2001 Markus Friedl.  All rights reserved.
+ * Copyright (c) 2001 Daniel Hartmeier.  All rights reserved.
+ * Copyright (c) 2001 Theo de Raadt.  All rights reserved.
+ *
+ * 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/types.h>
+#include <sys/queue.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+
+#include <netinet/in.h>
+
+#include <arpa/inet.h>
+
+#include <netdb.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <event.h>
+#include <ifaddrs.h>
+#include <imsg.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#include "got_error.h"
+#include "gotconfig.h"
+
+static struct file {
+	FILE			*stream;
+	const char		*name;
+	size_t	 		 ungetpos;
+	size_t			 ungetsize;
+	u_char			*ungetbuf;
+	int			 eof_reached;
+	int			 lineno;
+} *file;
+static const struct got_error*	newfile(struct file**, const char *, int *);
+static void	closefile(struct file *);
+int		 yyparse(void);
+int		 yylex(void);
+int		 yyerror(const char *, ...)
+    __attribute__((__format__ (printf, 1, 2)))
+    __attribute__((__nonnull__ (1)));
+int		 kw_cmp(const void *, const void *);
+int		 lookup(char *);
+int		 igetc(void);
+int		 lgetc(int);
+void		 lungetc(int);
+int		 findeol(void);
+static int	 parseport(char *, long long *);
+
+TAILQ_HEAD(symhead, sym)	 symhead = TAILQ_HEAD_INITIALIZER(symhead);
+struct sym {
+	TAILQ_ENTRY(sym)	 entry;
+	int			 used;
+	int			 persist;
+	char			*nam;
+	char			*val;
+};
+
+int	 symset(const char *, const char *, int);
+char	*symget(const char *);
+
+static int	 atoul(char *, u_long *);
+
+static const struct got_error* gerror;
+static struct gotconfig_remote_repo *remote;
+static struct gotconfig gotconfig;
+static const struct got_error* new_remote(struct gotconfig_remote_repo **);
+
+typedef struct {
+	union {
+		int64_t		 number;
+		char		*string;
+	} v;
+	int lineno;
+} YYSTYPE;
+
+%}
+
+%token	ERROR
+%token	REMOTE REPOSITORY SERVER PORT PROTOCOL MIRROR_REFERENCES AUTHOR
+%token	<v.string>	STRING
+%token	<v.number>	NUMBER
+%type	<v.number>	boolean portplain
+%type	<v.string>	numberstring
+
+%%
+
+grammar		: /* empty */
+		| grammar '\n'
+		| grammar author '\n'
+		| grammar remote '\n'
+		;
+boolean		: STRING {
+			if (strcasecmp($1, "true") == 0 ||
+			    strcasecmp($1, "yes") == 0)
+				$$ = 1;
+			else if (strcasecmp($1, "false") == 0 ||
+			    strcasecmp($1, "no") == 0)
+				$$ = 0;
+			else {
+				yyerror("invalid boolean value '%s'", $1);
+				free($1);
+				YYERROR;
+			}
+			free($1);
+		}
+		;
+numberstring	: NUMBER				{
+			char	*s;
+			if (asprintf(&s, "%lld", $1) == -1) {
+				yyerror("string: asprintf");
+				YYERROR;
+			}
+			$$ = s;
+		}
+		| STRING
+		;
+portplain	: numberstring	{
+			if (parseport($1, &$$) == -1) {
+				free($1);
+				YYERROR;
+			}
+			free($1);
+		}
+		;
+remoteopts2	: remoteopts2 remoteopts1 nl
+	   	| remoteopts1 optnl
+		;
+remoteopts1	: REPOSITORY STRING {
+	   		remote->repository = strdup($2);
+			if (remote->repository == NULL) {
+				free($2);
+				yyerror("strdup");
+				YYERROR;
+			}
+			free($2);
+	   	}
+	   	| SERVER STRING {
+	   		remote->server = strdup($2);
+			if (remote->server == NULL) {
+				free($2);
+				yyerror("strdup");
+				YYERROR;
+			}
+			free($2);
+		}
+		| PROTOCOL STRING {
+	   		remote->protocol = strdup($2);
+			if (remote->protocol == NULL) {
+				free($2);
+				yyerror("strdup");
+				YYERROR;
+			}
+			free($2);
+		}
+		| MIRROR_REFERENCES boolean {
+			remote->mirror_references = $2;
+		}
+		| PORT portplain {
+			remote->port = $2;
+		}
+	   	;
+remote		: REMOTE STRING {
+			static const struct got_error* error;
+
+			error = new_remote(&remote);
+			if (error) {
+				free($2);
+				yyerror("%s", error->msg);
+				YYERROR;
+			}
+			remote->name = strdup($2);
+			if (remote->name == NULL) {
+				free($2);
+				yyerror("strdup");
+				YYERROR;
+			}
+			free($2);
+		} '{' optnl remoteopts2 '}' {
+			TAILQ_INSERT_TAIL(&gotconfig.remotes, remote, entry);
+			gotconfig.nremotes++;
+		}
+		;
+author		: AUTHOR STRING {
+	   		gotconfig.author = strdup($2);
+			if (gotconfig.author == NULL) {
+				free($2);
+				yyerror("strdup");
+				YYERROR;
+			}
+			free($2);
+		}
+		;
+optnl		: '\n' optnl
+		| /* empty */
+		;
+nl		: '\n' optnl
+		;
+%%
+
+struct keywords {
+	const char	*k_name;
+	int		 k_val;
+};
+
+int
+yyerror(const char *fmt, ...)
+{
+	va_list		 ap;
+	char		*msg;
+	char		*err = NULL;
+
+	va_start(ap, fmt);
+	if (vasprintf(&msg, fmt, ap) == -1) {
+		gerror =  got_error_from_errno("vasprintf");
+		return 0;
+	}
+	va_end(ap);
+	if (asprintf(&err, "%s: line %d: %s", file->name, yylval.lineno,
+	    msg) == -1) {
+		gerror = got_error_from_errno("asprintf");
+		return(0);
+	}
+	gerror = got_error_msg(GOT_ERR_PARSE_CONFIG, strdup(err));
+	free(msg);
+	free(err);
+	return(0);
+}
+int
+kw_cmp(const void *k, const void *e)
+{
+	return (strcmp(k, ((const struct keywords *)e)->k_name));
+}
+
+int
+lookup(char *s)
+{
+	/* This has to be sorted always. */
+	static const struct keywords keywords[] = {
+		{"author",		AUTHOR},
+		{"mirror-references",	MIRROR_REFERENCES},
+		{"port",		PORT},
+		{"protocol",		PROTOCOL},
+		{"remote",		REMOTE},
+		{"repository",		REPOSITORY},
+		{"server",		SERVER},
+	};
+	const struct keywords	*p;
+
+	p = bsearch(s, keywords, sizeof(keywords)/sizeof(keywords[0]),
+	    sizeof(keywords[0]), kw_cmp);
+
+	if (p)
+		return (p->k_val);
+	else
+		return (STRING);
+}
+
+#define START_EXPAND	1
+#define DONE_EXPAND	2
+
+static int	expanding;
+
+int
+igetc(void)
+{
+	int	c;
+
+	while (1) {
+		if (file->ungetpos > 0)
+			c = file->ungetbuf[--file->ungetpos];
+		else
+			c = getc(file->stream);
+
+		if (c == START_EXPAND)
+			expanding = 1;
+		else if (c == DONE_EXPAND)
+			expanding = 0;
+		else
+			break;
+	}
+	return (c);
+}
+
+int
+lgetc(int quotec)
+{
+	int		c, next;
+
+	if (quotec) {
+		c = igetc();
+		if (c == EOF) {
+			yyerror("reached end of file while parsing "
+			    "quoted string");
+		}
+		return (c);
+	}
+
+	c = igetc();
+	while (c == '\\') {
+		next = igetc();
+		if (next != '\n') {
+			c = next;
+			break;
+		}
+		yylval.lineno = file->lineno;
+		file->lineno++;
+	}
+
+	return (c);
+}
+
+void
+lungetc(int c)
+{
+	if (c == EOF)
+		return;
+
+	if (file->ungetpos >= file->ungetsize) {
+		void *p = reallocarray(file->ungetbuf, file->ungetsize, 2);
+		if (p == NULL)
+			err(1, "%s", __func__);
+		file->ungetbuf = p;
+		file->ungetsize *= 2;
+	}
+	file->ungetbuf[file->ungetpos++] = c;
+}
+
+int
+findeol(void)
+{
+	int	c;
+
+	/* Skip to either EOF or the first real EOL. */
+	while (1) {
+		c = lgetc(0);
+		if (c == '\n') {
+			file->lineno++;
+			break;
+		}
+		if (c == EOF)
+			break;
+	}
+	return (ERROR);
+}
+
+static long long
+getservice(char *n)
+{
+	struct servent	*s;
+	u_long		 ulval;
+
+	if (atoul(n, &ulval) == 0) {
+		if (ulval > 65535) {
+			yyerror("illegal port value %lu", ulval);
+			return (-1);
+		}
+		return ulval;
+	} else {
+		s = getservbyname(n, "tcp");
+		if (s == NULL)
+			s = getservbyname(n, "udp");
+		if (s == NULL) {
+			yyerror("unknown port %s", n);
+			return (-1);
+		}
+		return (s->s_port);
+	}
+}
+
+static int
+parseport(char *port, long long *pn)
+{
+	if ((*pn = getservice(port)) == -1) {
+		*pn = 0LL;
+		return (-1);
+	}
+	return (0);
+}
+
+
+int
+yylex(void)
+{
+	unsigned char	 buf[8096];
+	unsigned char	*p, *val;
+	int		 quotec, next, c;
+	int		 token;
+
+top:
+	p = buf;
+	c = lgetc(0);
+	while (c == ' ' || c == '\t')
+		c = lgetc(0); /* nothing */
+
+	yylval.lineno = file->lineno;
+	if (c == '#') {
+		c = lgetc(0);
+		while (c != '\n' && c != EOF)
+			c = lgetc(0); /* nothing */
+	}
+	if (c == '$' && !expanding) {
+		while (1) {
+			c = lgetc(0);
+			if (c == EOF)
+				return (0);
+
+			if (p + 1 >= buf + sizeof(buf) - 1) {
+				yyerror("string too long");
+				return (findeol());
+			}
+			if (isalnum(c) || c == '_') {
+				*p++ = c;
+				continue;
+			}
+			*p = '\0';
+			lungetc(c);
+			break;
+		}
+		val = symget(buf);
+		if (val == NULL) {
+			yyerror("macro '%s' not defined", buf);
+			return (findeol());
+		}
+		p = val + strlen(val) - 1;
+		lungetc(DONE_EXPAND);
+		while (p >= val) {
+			lungetc(*p);
+			p--;
+		}
+		lungetc(START_EXPAND);
+		goto top;
+	}
+
+	switch (c) {
+	case '\'':
+	case '"':
+		quotec = c;
+		while (1) {
+			c = lgetc(quotec);
+			if (c == EOF)
+				return (0);
+			if (c == '\n') {
+				file->lineno++;
+				continue;
+			} else if (c == '\\') {
+				next = lgetc(quotec);
+				if (next == EOF)
+					return (0);
+				if (next == quotec || c == ' ' || c == '\t')
+					c = next;
+				else if (next == '\n') {
+					file->lineno++;
+					continue;
+				} else
+					lungetc(next);
+			} else if (c == quotec) {
+				*p = '\0';
+				break;
+			} else if (c == '\0') {
+				yyerror("syntax error");
+				return (findeol());
+			}
+			if (p + 1 >= buf + sizeof(buf) - 1) {
+				yyerror("string too long");
+				return (findeol());
+			}
+			*p++ = c;
+		}
+		yylval.v.string = strdup(buf);
+		if (yylval.v.string == NULL)
+			err(1, "%s", __func__);
+		return (STRING);
+	}
+
+#define allowed_to_end_number(x) \
+	(isspace(x) || x == ')' || x ==',' || x == '/' || x == '}' || x == '=')
+
+	if (c == '-' || isdigit(c)) {
+		do {
+			*p++ = c;
+			if ((unsigned)(p-buf) >= sizeof(buf)) {
+				yyerror("string too long");
+				return (findeol());
+			}
+			c = lgetc(0);
+		} while (c != EOF && isdigit(c));
+		lungetc(c);
+		if (p == buf + 1 && buf[0] == '-')
+			goto nodigits;
+		if (c == EOF || allowed_to_end_number(c)) {
+			const char *errstr = NULL;
+
+			*p = '\0';
+			yylval.v.number = strtonum(buf, LLONG_MIN,
+			    LLONG_MAX, &errstr);
+			if (errstr) {
+				yyerror("\"%s\" invalid number: %s",
+				    buf, errstr);
+				return (findeol());
+			}
+			return (NUMBER);
+		} else {
+nodigits:
+			while (p > buf + 1)
+				lungetc(*--p);
+			c = *--p;
+			if (c == '-')
+				return (c);
+		}
+	}
+
+#define allowed_in_string(x) \
+	(isalnum(x) || (ispunct(x) && x != '(' && x != ')' && \
+	x != '{' && x != '}' && \
+	x != '!' && x != '=' && x != '#' && \
+	x != ','))
+
+	if (isalnum(c) || c == ':' || c == '_') {
+		do {
+			*p++ = c;
+			if ((unsigned)(p-buf) >= sizeof(buf)) {
+				yyerror("string too long");
+				return (findeol());
+			}
+			c = lgetc(0);
+		} while (c != EOF && (allowed_in_string(c)));
+		lungetc(c);
+		*p = '\0';
+		token = lookup(buf);
+		if (token == STRING) {
+			yylval.v.string = strdup(buf);
+			if (yylval.v.string == NULL)
+				err(1, "%s", __func__);
+		}
+		return (token);
+	}
+	if (c == '\n') {
+		yylval.lineno = file->lineno;
+		file->lineno++;
+	}
+	if (c == EOF)
+		return (0);
+	return (c);
+}
+
+static const struct got_error*
+newfile(struct file **nfile, const char *filename, int *fd)
+{
+	const struct got_error* error = NULL;
+
+	(*nfile) = calloc(1, sizeof(struct file));
+	if ((*nfile) == NULL)
+		return got_error_from_errno("calloc");
+	(*nfile)->stream = fdopen(*fd, "r");
+	if ((*nfile)->stream == NULL) {
+		error = got_error_from_errno("fdopen");
+		free((*nfile));
+		return error;
+	}
+	*fd = -1; /* Stream owns the file descriptor now. */
+	(*nfile)->name = filename;
+	(*nfile)->lineno = 1;
+	(*nfile)->ungetsize = 16;
+	(*nfile)->ungetbuf = malloc((*nfile)->ungetsize);
+	if ((*nfile)->ungetbuf == NULL) {
+		error = got_error_from_errno("malloc");
+		fclose((*nfile)->stream);
+		free((*nfile));
+		return error;
+	}
+	return NULL;
+}
+
+static const struct got_error*
+new_remote(struct gotconfig_remote_repo **remote)
+{
+	const struct got_error *error = NULL;
+
+	*remote = calloc(1, sizeof(**remote));
+	if (*remote == NULL)
+		error = got_error_from_errno("calloc");
+	return error;
+}
+
+static void
+closefile(struct file *file)
+{
+	fclose(file->stream);
+	free(file->ungetbuf);
+	free(file);
+}
+
+const struct got_error *
+gotconfig_parse(struct gotconfig **conf, const char *filename, int *fd)
+{
+	const struct got_error *err = NULL;
+	struct sym	*sym, *next;
+
+	*conf = NULL;
+
+	err = newfile(&file, filename, fd);
+	if (err)
+		return err;
+
+	TAILQ_INIT(&gotconfig.remotes);
+
+	yyparse();
+	closefile(file);
+
+	/* Free macros and check which have not been used. */
+	TAILQ_FOREACH_SAFE(sym, &symhead, entry, next) {
+		if (!sym->persist) {
+			free(sym->nam);
+			free(sym->val);
+			TAILQ_REMOVE(&symhead, sym, entry);
+			free(sym);
+		}
+	}
+
+	if (gerror == NULL)
+		*conf = &gotconfig;
+	return gerror;
+}
+
+void
+gotconfig_free(struct gotconfig *conf)
+{
+	struct gotconfig_remote_repo *remote;
+
+	free(conf->author);
+	while (!TAILQ_EMPTY(&conf->remotes)) {
+		remote = TAILQ_FIRST(&conf->remotes);
+		TAILQ_REMOVE(&conf->remotes, remote, entry);
+		free(remote->name);
+		free(remote->repository);
+		free(remote->server);
+		free(remote->protocol);
+		free(remote);
+	}
+}
+
+int
+symset(const char *nam, const char *val, int persist)
+{
+	struct sym	*sym;
+
+	TAILQ_FOREACH(sym, &symhead, entry) {
+		if (strcmp(nam, sym->nam) == 0)
+			break;
+	}
+
+	if (sym != NULL) {
+		if (sym->persist == 1)
+			return (0);
+		else {
+			free(sym->nam);
+			free(sym->val);
+			TAILQ_REMOVE(&symhead, sym, entry);
+			free(sym);
+		}
+	}
+	sym = calloc(1, sizeof(*sym));
+	if (sym == NULL)
+		return (-1);
+
+	sym->nam = strdup(nam);
+	if (sym->nam == NULL) {
+		free(sym);
+		return (-1);
+	}
+	sym->val = strdup(val);
+	if (sym->val == NULL) {
+		free(sym->nam);
+		free(sym);
+		return (-1);
+	}
+	sym->used = 0;
+	sym->persist = persist;
+	TAILQ_INSERT_TAIL(&symhead, sym, entry);
+	return (0);
+}
+
+int
+cmdline_symset(char *s)
+{
+	char	*sym, *val;
+	int	ret;
+	size_t	len;
+
+	val = strrchr(s, '=');
+	if (val == NULL)
+		return (-1);
+
+	len = strlen(s) - strlen(val) + 1;
+	sym = malloc(len);
+	if (sym == NULL)
+		errx(1, "cmdline_symset: malloc");
+
+	strlcpy(sym, s, len);
+
+	ret = symset(sym, val + 1, 1);
+	free(sym);
+
+	return (ret);
+}
+
+char *
+symget(const char *nam)
+{
+	struct sym	*sym;
+
+	TAILQ_FOREACH(sym, &symhead, entry) {
+		if (strcmp(nam, sym->nam) == 0) {
+			sym->used = 1;
+			return (sym->val);
+		}
+	}
+	return (NULL);
+}
+
+static int
+atoul(char *s, u_long *ulvalp)
+{
+	u_long	 ulval;
+	char	*ep;
+
+	errno = 0;
+	ulval = strtoul(s, &ep, 0);
+	if (s[0] == '\0' || *ep != '\0')
+		return (-1);
+	if (errno == ERANGE && ulval == ULONG_MAX)
+		return (-1);
+	*ulvalp = ulval;
+	return (0);
+}
blob - 34abaa95cade02c6d2fc7fe82a0cae32a089e962
blob + 13a4e58bcc3693cd4eb419089a8e29ea5e377524
--- regress/cmdline/commit.sh
+++ regress/cmdline/commit.sh
@@ -695,7 +695,44 @@ function test_commit_tree_entry_sorting {
 	# Let git-fsck verify the newly written tree to make sure Git is happy
 	(cd $testroot/repo && git fsck --strict  \
 		> $testroot/fsck.stdout 2> $testroot/fsck.stderr)
+	ret="$?"
+	test_done "$testroot" "$ret"
+}
+
+function test_commit_gotconfig_author {
+	local testroot=`test_init commit_gotconfig_author`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	echo 'author "Flan Luck <flan_luck@openbsd.org>"' \
+		> $testroot/repo/.git/got.conf
+
+	echo "modified alpha" > $testroot/wt/alpha
+	(cd $testroot/wt && got commit -m 'test gotconfig author' > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/repo && got log -l1 | grep ^from: > $testroot/stdout)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "from: Flan Luck <flan_luck@openbsd.org>" \
+		> $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
 	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
 	test_done "$testroot" "$ret"
 }
 
@@ -1297,6 +1334,7 @@ run_test test_commit_selected_paths
 run_test test_commit_outside_refs_heads
 run_test test_commit_no_email
 run_test test_commit_tree_entry_sorting
+run_test test_commit_gotconfig_author
 run_test test_commit_gitconfig_author
 run_test test_commit_xbit_change
 run_test test_commit_normalizes_filemodes