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

From:
Tracey Emery <tracey@traceyemery.net>
Subject:
Re: add got.conf(5)
To:
Stefan Sperling <stsp@stsp.name>
Cc:
gameoftrees@openbsd.org
Date:
Wed, 9 Sep 2020 15:58:00 -0600

Download raw body.

Thread
On Sun, Sep 06, 2020 at 06:09:44PM +0200, Stefan Sperling wrote:
> This adds a got.conf(5) configuration file, based on parse.y code
> added by Tracey in the got/ subdirectory.
> 
> The new got.conf file is parsed by a new libexec helper when a repository
> is opened. This is not done because we really need privsep, but because
> yacc generates code with global variables that cannot be used in a library
> context. We can work around this by running our parse.y code in a separate
> address space.
> 
> At present, only author information (for got commit/import) and remote
> repositories (for got fetch) can be configured. We can expand this scope
> later as needed.
> 
> ok?

Ok. There are some whitespace problems pointed out below, but those can
be handled after the commit. Also, this diff doesn't take care of
removing got/parse.y. Were you going to handle that after the commit?

> 
> diff refs/heads/master refs/heads/gotconfig
> blob - 19122e372a158d8a95216bb854924e9a25225a76
> blob + f74cc058dc06317e6752bfcbebd77f4e23bccba8
> --- 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,6 +188,8 @@ 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
>  .Ar repository-url
> @@ -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,19 +1930,25 @@ 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
>  .Dv user.name
>  and
>  .Dv user.email
> -configuration settings contained in Git's global
> +configuration settings contained in
> +.Xr got.conf 5
> +or Git's global
>  .Pa ~/.gitconfig
>  configuration file will be used only if the
>  .Ev GOT_AUTHOR
> @@ -1951,6 +1967,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 +2283,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 + ee68786d7a1608cf5b225ba31744511639efc4f6
> --- 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,16 +1901,24 @@ 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;
> +		}
> +	}	

extra end tab

>  
>  	error = got_fetch_parse_uri(&proto, &host, &port, &server_path,
>  	    &repo_name, remote->url);
> 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 - 44f5cc811353036797f501a79e4fb1ee0f11dcf1
> blob + b91bda4fa54fee65a9eb14a87a3662af9a624a11
> --- lib/privsep.c
> +++ lib/privsep.c
> @@ -1887,6 +1887,208 @@ got_privsep_recv_gitconfig_remotes(struct got_remote_r
>  }
>  
>  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));
> +	

extra tab

> +		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)
>  {
> @@ -2008,6 +2210,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 - b8408a749c9e5ff2c1983eb5d006ed171fdfb6e8
> blob + 794725948774f714efc8876112a3a0da332e47be
> --- lib/repository.c
> +++ lib/repository.c
> @@ -159,6 +159,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)
> @@ -167,6 +173,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)
>  {
> @@ -516,6 +536,124 @@ done:
>  	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;
> +}
> +
>  const struct got_error *
>  got_repo_open(struct got_repository **repop, const char *path,
>      const char *global_gitconfig_path)
> @@ -589,6 +727,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;
> @@ -644,6 +786,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 + ff7e52b3c971f60ef8eea16eaa9fb51f78f337bb (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);
> +		}
> +	}	

extra tab

> +
> +	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 + ea6e5db706853103c807b6c22841c8aba8770b71 (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

4x spaces

> +		;
> +remoteopts1	: REPOSITORY STRING {
> +	    		remote->repository = strdup($2);

4x spaces

> +			if (remote->repository == NULL) {
> +				free($2);
> +				yyerror("strdup");
> +				YYERROR;
> +			}
> +			free($2);
> +	    	}

4x spaces

> +	    	| SERVER STRING {

4x spaces

> +	    		remote->server = strdup($2);

4x spaces

> +			if (remote->server == NULL) {
> +				free($2);
> +				yyerror("strdup");
> +				YYERROR;
> +			}
> +			free($2);
> +		}
> +		| PROTOCOL STRING {
> +	    		remote->protocol = strdup($2);

4x spaces

> +			if (remote->protocol == NULL) {
> +				free($2);
> +				yyerror("strdup");
> +				YYERROR;
> +			}
> +			free($2);
> +		}
> +		| MIRROR_REFERENCES boolean {
> +			remote->mirror_references = $2;
> +		}
> +		| PORT portplain {
> +			remote->port = $2;
> +		}
> +	    	;

4x spaces

> +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);

4x spaces

> +			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
> @@ -699,6 +699,43 @@ function test_commit_tree_entry_sorting {
>  	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"
> +}
> +
>  function test_commit_gitconfig_author {
>  	local testroot=`test_init commit_gitconfig_author`
>  
> @@ -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

-- 

Tracey Emery