commit 624411d2da8f88b37d2b440833ce76fa6705bdd3 from: Josh Rickmar date: Sat Jul 08 13:12:44 2023 UTC Copy got to cvg commit - 4b3827cd43394b89d2af822dcd1d9a9179c1ee10 commit + 624411d2da8f88b37d2b440833ce76fa6705bdd3 blob - /dev/null blob + 3a08950d455ea0bc1854f88ea4f006059b16a199 (mode 644) --- /dev/null +++ cvg/Makefile @@ -0,0 +1,40 @@ +.PATH:${.CURDIR}/../lib + +.include "../got-version.mk" + +PROG= got +SRCS= got.c blame.c commit_graph.c delta.c diff.c \ + diffreg.c error.c fileindex.c object.c object_cache.c \ + object_idset.c object_parse.c opentemp.c path.c pack.c \ + privsep.c reference.c repository.c hash.c worktree.c \ + worktree_open.c inflate.c buf.c rcsutil.c diff3.c lockfile.c \ + deflate.c object_create.c delta_cache.c fetch.c \ + gotconfig.c diff_main.c diff_atomize_text.c \ + diff_myers.c diff_output.c diff_output_plain.c \ + diff_output_unidiff.c diff_output_edscript.c \ + diff_patience.c send.c deltify.c pack_create.c dial.c \ + bloom.c murmurhash2.c ratelimit.c patch.c sigs.c date.c \ + object_open_privsep.c read_gitconfig_privsep.c \ + read_gotconfig_privsep.c pack_create_privsep.c pollfd.c \ + reference_parse.c object_qid.c + +MAN = ${PROG}.1 got-worktree.5 git-repository.5 got.conf.5 + +CPPFLAGS = -I${.CURDIR}/../include -I${.CURDIR}/../lib + +.if defined(PROFILE) +LDADD = -lutil_p -lz_p -lm_p -lc_p +.else +LDADD = -lutil -lz -lm +.endif +DPADD = ${LIBZ} ${LIBUTIL} ${LIBM} + +.if ${GOT_RELEASE} != "Yes" +NOMAN = Yes +.endif + +realinstall: + ${INSTALL} ${INSTALL_COPY} -o ${BINOWN} -g ${BINGRP} \ + -m ${BINMODE} ${PROG} ${BINDIR}/${PROG} + +.include blob - /dev/null blob + ea3b833f1bc15d5ea3e276e75da0a16f1e29702c (mode 644) --- /dev/null +++ cvg/cvg.1 @@ -0,0 +1,3817 @@ +.\" +.\" Copyright (c) 2017 Martin Pieuchot +.\" Copyright (c) 2018, 2019, 2020 Stefan Sperling +.\" +.\" 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 1 +.Os +.Sh NAME +.Nm got +.Nd Game of Trees +.Sh SYNOPSIS +.Nm +.Op Fl hV +.Ar command +.Op Ar arg ... +.Sh DESCRIPTION +.Nm +is a version control system which stores the history of tracked files +in a Git repository, as used by the Git version control system. +This repository format is described in +.Xr git-repository 5 . +.Pp +.Nm +is a +.Dq distributed +version control system because every copy of a repository is writeable. +Modifications made to files can be synchronized between repositories +at any time. +.Pp +Files managed by +.Nm +must be checked out from the repository for modification. +Checked out files are stored in a +.Em work tree +which can be placed at an arbitrary directory in the filesystem hierarchy. +The on-disk format of this work tree is described in +.Xr got-worktree 5 . +.Pp +.Nm +provides global and command-specific options. +Global options must precede the command name, and are as follows: +.Bl -tag -width tenletters +.It Fl h +Display usage information and exit immediately. +.It Fl V , -version +Display program version and exit immediately. +.El +.Pp +The commands for +.Nm +are as follows: +.Bl -tag -width checkout +.Tg im +.It Xo +.Cm import +.Op Fl b Ar branch +.Op Fl I Ar pattern +.Op Fl m Ar message +.Op Fl r Ar repository-path +.Ar directory +.Xc +.Dl Pq alias: Cm im +Create an initial commit in a repository from the file hierarchy +within the specified +.Ar directory . +The created commit will not have any parent commits, i.e. it will be a +root commit. +Also create a new reference which provides a branch name for the newly +created commit. +Show the path of each imported file to indicate progress. +.Pp +The +.Cm got import +command requires the +.Ev GOT_AUTHOR +environment variable to be set, +unless an author has been configured in +.Xr got.conf 5 +or Git's +.Dv user.name +and +.Dv user.email +configuration settings can be obtained from the repository's +.Pa .git/config +file or from Git's global +.Pa ~/.gitconfig +configuration file. +.Pp +The options for +.Cm got import +are as follows: +.Bl -tag -width Ds +.It Fl b Ar branch +Create the specified +.Ar branch . +If this option is not specified, a branch corresponding to the repository's +HEAD reference will be used. +Use of this option is required if the branch resolved via the repository's +HEAD reference already exists. +.It Fl I Ar pattern +Ignore files or directories with a name which matches the specified +.Ar pattern . +This option may be specified multiple times to build a list of ignore patterns. +The +.Ar pattern +follows the globbing rules documented in +.Xr glob 7 . +Ignore patterns which end with a slash, +.Dq / , +will only match directories. +.It Fl m Ar message +Use the specified log message when creating the new commit. +Without the +.Fl m +option, +.Cm got import +opens a temporary file in an editor where a log message can be written. +.It Fl r Ar repository-path +Use the repository at the specified path. +If not specified, assume the repository is located at or above the current +working directory. +.El +.Tg cl +.It Xo +.Cm clone +.Op Fl almqv +.Op Fl b Ar branch +.Op Fl R Ar reference +.Ar repository-URL +.Op Ar directory +.Xc +.Dl Pq alias: Cm cl +Clone a Git repository at the specified +.Ar repository-URL +into the specified +.Ar directory . +If no +.Ar directory +is specified, the directory name will be derived from the name of the +cloned repository. +.Cm got clone +will refuse to run if the +.Ar directory +already exists. +.Pp +The +.Ar repository-URL +specifies a protocol scheme, a server hostname, an optional port number +separated from the hostname by a colon, and a path to the repository on +the server: +.Lk scheme://hostname:port/path/to/repository +.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 +.Pp +Objects in the cloned repository are stored in a pack file which is downloaded +from the server. +This pack file will then be indexed to facilitate access to the objects stored +within. +If any objects in the pack file are stored in deltified form, all deltas will +be fully resolved in order to compute the ID of such objects. +This can take some time. +More details about the pack file format are documented in +.Xr git-repository 5 . +.Pp +.Cm got clone +creates a remote repository entry in the +.Xr got.conf 5 +and +.Pa config +files of the cloned repository to store the +.Ar repository-url +and any +.Ar branch +or +.Ar reference +arguments for future use by +.Cm got fetch +or +.Xr git-fetch 1 . +.Pp +The options for +.Cm got clone +are as follows: +.Bl -tag -width Ds +.It Fl a +Fetch all branches from the remote repository's +.Dq refs/heads/ +reference namespace and set +.Cm fetch_all_branches +in the cloned repository's +.Xr got.conf 5 +file for future use by +.Cm got fetch . +If this option is not specified, a branch resolved via the remote +repository's HEAD reference will be fetched. +Cannot be used together with the +.Fl b +option. +.It Fl b Ar branch +Fetch the specified +.Ar branch +from the remote repository's +.Dq refs/heads/ +reference namespace. +This option may be specified multiple times to build a list of branches +to fetch. +If the branch corresponding to the remote repository's HEAD reference is not +in this list, the cloned repository's HEAD reference will be set to the first +branch which was fetched. +If this option is not specified, a branch resolved via the remote +repository's HEAD reference will be fetched. +Cannot be used together with the +.Fl a +option. +.It Fl l +List branches and tags available for fetching from the remote repository +and exit immediately. +Cannot be used together with any of the other options except +.Fl q +and +.Fl v . +.It Fl m +Create the cloned repository as a mirror of the original repository. +This is useful if the cloned repository will not be used to store +locally created commits. +.Pp +The repository's +.Xr got.conf 5 +and +.Pa config +files will be set up with the +.Dq mirror +option enabled, such that +.Cm got fetch +or +.Xr git-fetch 1 +will write incoming changes directly to branches in the +.Dq refs/heads/ +reference namespace, rather than to branches in the +.Dq refs/remotes/ +namespace. +This avoids the usual requirement of having to run +.Cm got rebase +or +.Cm got merge +after +.Cm got fetch +in order to make incoming changes appear on branches in the +.Dq refs/heads/ +namespace. +But maintaining custom changes in the cloned repository becomes difficult +since such changes will be at risk of being discarded whenever incoming +changes are fetched. +.It Fl q +Suppress progress reporting output. +The same option will be passed to +.Xr ssh 1 +if applicable. +.It Fl R Ar reference +In addition to the branches and tags that will be fetched, fetch an arbitrary +.Ar reference +from the remote repository's +.Dq refs/ +namespace. +This option may be specified multiple times to build a list of additional +references to fetch. +The specified +.Ar reference +may either be a path to a specific reference, or a reference namespace +which will cause all references in this namespace to be fetched. +.Pp +Each reference will be mapped into the cloned repository's +.Dq refs/remotes/ +namespace, unless the +.Fl m +option is used to mirror references directly into the cloned repository's +.Dq refs/ +namespace. +.Pp +.Cm got clone +will refuse to fetch references from the remote repository's +.Dq refs/remotes/ +or +.Dq refs/got/ +namespace. +.It Fl v +Verbose mode. +Causes +.Cm got clone +to print debugging messages to standard error output. +This option will be passed to +.Xr ssh 1 +if applicable. +Multiple -v options increase the verbosity. +The maximum is 3. +.El +.Tg fe +.It Xo +.Cm fetch +.Op Fl adlqtvX +.Op Fl b Ar branch +.Op Fl R Ar reference +.Op Fl r Ar repository-path +.Op Ar remote-repository +.Xc +.Dl Pq alias: Cm fe +Fetch new changes from a remote repository. +If no +.Ar remote-repository +is specified, +.Dq origin +will be used. +The remote repository's URL is obtained from the corresponding entry in +.Xr got.conf 5 +or Git's +.Pa config +file of the local repository, as created by +.Cm got clone . +.Pp +By default, any branches configured in +.Xr got.conf 5 +for the +.Ar remote-repository +will be fetched. +If +.Cm got fetch +is invoked in a work tree then this work tree's current branch will be +fetched, too, provided it is present on the server. +If no branches to fetch can be found in +.Xr got.conf 5 +or via a work tree, or said branches are not found on the server, a branch +resolved via the remote repository's HEAD reference will be fetched. +Likewise, if a HEAD reference for the +.Ar remote-repository +exists but its target no longer matches the remote HEAD, then +the new target branch will be fetched. +This default behaviour can be overridden with the +.Fl a +and +.Fl b +options. +.Pp +New changes will be stored in a separate pack file downloaded from the server. +Optionally, separate pack files stored in the repository can be combined with +.Xr git-repack 1 . +.Pp +By default, branch references in the +.Dq refs/remotes/ +reference namespace will be updated to point at the newly fetched commits. +The +.Cm got rebase +or +.Cm got merge +command can then be used to make new changes visible on branches in the +.Dq refs/heads/ +namespace, merging incoming changes with the changes on those branches +as necessary. +.Pp +If the repository was created as a mirror with +.Cm got clone -m , +then all branches in the +.Dq refs/heads/ +namespace will be updated directly to match the corresponding branches in +the remote repository. +If those branches contained local commits, these commits will no longer be +reachable via a reference and will therefore be at risk of being discarded +by Git's garbage collector or +.Cm gotadmin cleanup . +Maintaining custom changes in a mirror repository is therefore discouraged. +.Pp +In any case, references in the +.Dq refs/tags/ +namespace will always be fetched and mapped directly to local references +in the same namespace. +.Pp +The options for +.Cm got fetch +are as follows: +.Bl -tag -width Ds +.It Fl a +Fetch all branches from the remote repository's +.Dq refs/heads/ +reference namespace. +This option can be enabled by default for specific repositories in +.Xr got.conf 5 . +Cannot be used together with the +.Fl b +option. +.It Fl b Ar branch +Fetch the specified +.Ar branch +from the remote repository's +.Dq refs/heads/ +reference namespace. +This option may be specified multiple times to build a list of branches +to fetch. +Cannot be used together with the +.Fl a +option. +.It Fl d +Delete branches and tags from the local repository which are no longer +present in the remote repository. +Only references are deleted. +Any commit, tree, tag, and blob objects belonging to deleted branches or +tags remain in the repository and may be removed separately with +Git's garbage collector or +.Cm gotadmin cleanup . +.It Fl l +List branches and tags available for fetching from the remote repository +and exit immediately. +Cannot be used together with any of the other options except +.Fl v , +.Fl q , +and +.Fl r . +.It Fl q +Suppress progress reporting output. +The same option will be passed to +.Xr ssh 1 +if applicable. +.It Fl R Ar reference +In addition to the branches and tags that will be fetched, fetch an arbitrary +.Ar reference +from the remote repository's +.Dq refs/ +namespace. +This option may be specified multiple times to build a list of additional +references to fetch. +The specified +.Ar reference +may either be a path to a specific reference, or a reference namespace +which will cause all references in this namespace to be fetched. +.Pp +Each reference will be mapped into the local repository's +.Dq refs/remotes/ +namespace, unless the local repository was created as a mirror with +.Cm got clone -m +in which case references will be mapped directly into the local repository's +.Dq refs/ +namespace. +.Pp +Once a reference has been fetched, a branch based on it can be created with +.Cm got branch +if needed. +.Pp +.Cm got fetch +will refuse to fetch references from the remote repository's +.Dq refs/remotes/ +or +.Dq refs/got/ +namespace. +.It Fl r Ar repository-path +Use the repository at the specified path. +If not specified, assume the repository is located at or above the current +working directory. +If this directory is a +.Nm +work tree, use the repository path associated with this work tree. +.It Fl t +Allow existing references in the +.Dq refs/tags +namespace to be updated if they have changed on the server. +If not specified, only new tag references will be created. +.It Fl v +Verbose mode. +Causes +.Cm got fetch +to print debugging messages to standard error output. +The same option will be passed to +.Xr ssh 1 +if applicable. +Multiple -v options increase the verbosity. +The maximum is 3. +.It Fl X +Delete all references which correspond to a particular +.Ar remote-repository +instead of fetching new changes. +This can be useful when a remote repository is being removed from +.Xr got.conf 5 . +.Pp +With +.Fl X , +the +.Ar remote-repository +argument is mandatory and no other options except +.Fl r , +.Fl v , +and +.Fl q +are allowed. +.Pp +Only references are deleted. +Any commit, tree, tag, and blob objects fetched from a remote repository +will generally be stored in pack files and may be removed separately with +.Xr git-repack 1 +and Git's garbage collector. +.El +.Tg co +.It Xo +.Cm checkout +.Op Fl Eq +.Op Fl b Ar branch +.Op Fl c Ar commit +.Op Fl p Ar path-prefix +.Ar repository-path +.Op Ar work-tree-path +.Xc +.Dl Pq alias: Cm co +Copy files from a repository into a new work tree. +Show the status of each affected file, using the following status codes: +.Bl -column YXZ description +.It A Ta new file was added +.It E Ta file already exists in work tree's meta-data +.El +.Pp +If the +.Ar work tree path +is not specified, either use the last component of +.Ar repository path , +or if a +.Ar path prefix +was specified use the last component of +.Ar path prefix . +.Pp +The options for +.Cm got checkout +are as follows: +.Bl -tag -width Ds +.It Fl b Ar branch +Check out files from a commit on the specified +.Ar branch . +If this option is not specified, a branch resolved via the repository's HEAD +reference will be used. +.It Fl c Ar commit +Check out files from the specified +.Ar commit +on the selected branch. +The expected argument is a commit ID SHA1 hash or an existing reference +or tag name which will be resolved to a commit ID. +An abbreviated hash argument will be expanded to a full SHA1 hash +automatically, provided the abbreviation is unique. +If this option is not specified, the most recent commit on the selected +branch will be used. +.Pp +If the specified +.Ar commit +is not contained in the selected branch, a different branch which contains +this commit must be specified with the +.Fl b +option. +If no such branch is known, a new branch must be created for this +commit with +.Cm got branch +before +.Cm got checkout +can be used. +Checking out work trees with an unknown branch is intentionally not supported. +.It Fl E +Proceed with the checkout operation even if the directory at +.Ar work-tree-path +is not empty. +Existing files will be left intact. +.It Fl p Ar path-prefix +Restrict the work tree to a subset of the repository's tree hierarchy. +Only files beneath the specified +.Ar path-prefix +will be checked out. +.It Fl q +Silence progress output. +.El +.Tg up +.It Xo +.Cm update +.Op Fl q +.Op Fl b Ar branch +.Op Fl c Ar commit +.Op Ar path ... +.Xc +.Dl Pq alias: Cm up +Update an existing work tree to a different +.Ar commit . +Change existing files in the work tree as necessary to match file contents +of this commit. +Preserve any local changes in the work tree and merge them with the +incoming changes. +.Pp +Files which already contain merge conflicts will not be updated to avoid +further complications. +Such files will be updated when +.Cm got update +is run again after merge conflicts have been resolved. +If the conflicting changes are no longer needed, affected files can be +reverted with +.Cm got revert +before running +.Cm got update +again. +.Pp +Show the status of each affected file, using the following status codes: +.Bl -column YXZ description +.It U Ta file was updated and contained no local changes +.It G Ta file was updated and local changes were merged cleanly +.It C Ta file was updated and conflicts occurred during merge +.It D Ta file was deleted +.It d Ta file's deletion was prevented by local modifications +.It A Ta new file was added +.It \(a~ Ta versioned file is obstructed by a non-regular file +.It ! Ta a missing versioned file was restored +.It # Ta file was not updated because it contains merge conflicts +.It ? Ta changes destined for an unversioned file were not merged +.El +.Pp +If no +.Ar path +is specified, update the entire work tree. +Otherwise, restrict the update operation to files at or within the +specified paths. +Each path is required to exist in the update operation's target commit. +Files in the work tree outside specified paths will remain unchanged and +will retain their previously recorded base commit. +Some +.Nm +commands may refuse to run while the work tree contains files from +multiple base commits. +The base commit of such a work tree can be made consistent by running +.Cm got update +across the entire work tree. +Specifying a +.Ar path +is incompatible with the +.Fl b +option. +.Pp +.Cm got update +cannot update paths with staged changes. +If changes have been staged with +.Cm got stage , +these changes must first be committed with +.Cm got commit +or unstaged with +.Cm got unstage . +.Pp +The options for +.Cm got update +are as follows: +.Bl -tag -width Ds +.It Fl b Ar branch +Switch the work tree's branch reference to the specified +.Ar branch +before updating the work tree. +This option requires that all paths in the work tree are updated. +.Pp +As usual, any local changes in the work tree will be preserved. +This can be useful when switching to a newly created branch in order +to commit existing local changes to this branch. +.Pp +Any local changes must be dealt with separately in order to obtain a +work tree with pristine file contents corresponding exactly to the specified +.Ar branch . +Such changes could first be committed to a different branch with +.Cm got commit , +or could be discarded with +.Cm got revert . +.It Fl c Ar commit +Update the work tree to the specified +.Ar commit . +The expected argument is a commit ID SHA1 hash or an existing reference +or tag name which will be resolved to a commit ID. +An abbreviated hash argument will be expanded to a full SHA1 hash +automatically, provided the abbreviation is unique. +If this option is not specified, the most recent commit on the work tree's +branch will be used. +.It Fl q +Silence progress output. +.El +.Tg st +.It Xo +.Cm status +.Op Fl I +.Op Fl S Ar status-codes +.Op Fl s Ar status-codes +.Op Ar path ... +.Xc +.Dl Pq alias: Cm st +Show the current modification status of files in a work tree, +using the following status codes: +.Bl -column YXZ description +.It M Ta modified file +.It A Ta file scheduled for addition in next commit +.It D Ta file scheduled for deletion in next commit +.It C Ta modified or added file which contains merge conflicts +.It ! Ta versioned file was expected on disk but is missing +.It \(a~ Ta versioned file is obstructed by a non-regular file +.It ? Ta unversioned item not tracked by +.Nm +.It m Ta modified file modes (executable bit only) +.It N Ta non-existent +.Ar path +specified on the command line +.El +.Pp +If no +.Ar path +is specified, show modifications in the entire work tree. +Otherwise, show modifications at or within the specified paths. +.Pp +If changes have been staged with +.Cm got stage , +staged changes are shown in the second output column, using the following +status codes: +.Bl -column YXZ description +.It M Ta file modification is staged +.It A Ta file addition is staged +.It D Ta file deletion is staged +.El +.Pp +Changes created on top of staged changes are indicated in the first column: +.Bl -column YXZ description +.It MM Ta file was modified after earlier changes have been staged +.It MA Ta file was modified after having been staged for addition +.El +.Pp +The options for +.Cm got status +are as follows: +.Bl -tag -width Ds +.It Fl I +Show unversioned files even if they match an ignore pattern. +.It Fl S Ar status-codes +Suppress the output of files with a modification status matching any of the +single-character status codes contained in the +.Ar status-codes +argument. +Any combination of codes from the above list of possible status codes +may be specified. +For staged files, status codes displayed in either column will be matched. +Cannot be used together with the +.Fl s +option. +.It Fl s Ar status-codes +Only show files with a modification status matching any of the +single-character status codes contained in the +.Ar status-codes +argument. +Any combination of codes from the above list of possible status codes +may be specified. +For staged files, status codes displayed in either column will be matched. +Cannot be used together with the +.Fl S +option. +.El +.Pp +For compatibility with +.Xr cvs 1 +and +.Xr git 1 , +.Cm got status +reads +.Xr glob 7 +patterns from +.Pa .cvsignore +and +.Pa .gitignore +files in each traversed directory and will not display unversioned files +which match these patterns. +Ignore patterns which end with a slash, +.Dq / , +will only match directories. +As an extension to +.Xr glob 7 +matching rules, +.Cm got status +supports consecutive asterisks, +.Dq ** , +which will match an arbitrary amount of directories. +Unlike +.Xr cvs 1 , +.Cm got status +only supports a single ignore pattern per line. +Unlike +.Xr git 1 , +.Cm got status +does not support negated ignore patterns prefixed with +.Dq \&! , +and gives no special significance to the location of path component separators, +.Dq / , +in a pattern. +.It Xo +.Cm log +.Op Fl bdPpRs +.Op Fl C Ar number +.Op Fl c Ar commit +.Op Fl l Ar N +.Op Fl r Ar repository-path +.Op Fl S Ar search-pattern +.Op Fl x Ar commit +.Op Ar path +.Xc +Display history of a repository. +If a +.Ar path +is specified, show only commits which modified this path. +If invoked in a work tree, the +.Ar path +is interpreted relative to the current working directory, +and the work tree's path prefix is implicitly prepended. +Otherwise, the path is interpreted relative to the repository root. +.Pp +The options for +.Cm got log +are as follows: +.Bl -tag -width Ds +.It Fl b +Display individual commits which were merged into the current branch +from other branches. +By default, +.Cm got log +shows the linear history of the current branch only. +.It Fl C Ar number +Set the number of context lines shown in diffs with +.Fl p . +By default, 3 lines of context are shown. +.It Fl c Ar commit +Start traversing history at the specified +.Ar commit . +The expected argument is a commit ID SHA1 hash or an existing reference +or tag name which will be resolved to a commit ID. +An abbreviated hash argument will be expanded to a full SHA1 hash +automatically, provided the abbreviation is unique. +If this option is not specified, default to the work tree's current branch +if invoked in a work tree, or to the repository's HEAD reference. +.It Fl d +Display diffstat of changes introduced in each commit. +Cannot be used with the +.Fl s +option. +.It Fl l Ar N +Limit history traversal to a given number of commits. +If this option is not specified, a default limit value of zero is used, +which is treated as an unbounded limit. +The +.Ev GOT_LOG_DEFAULT_LIMIT +environment variable may be set to change this default value. +.It Fl P +Display the list of file paths changed in each commit, using the following +status codes: +.Bl -column YXZ description +.It M Ta modified file +.It D Ta file was deleted +.It A Ta new file was added +.It m Ta modified file modes (executable bit only) +.El +.Pp +Cannot be used with the +.Fl s +option. +.It Fl p +Display the patch of modifications made in each commit. +If a +.Ar path +is specified, only show the patch of modifications at or within this path. +Cannot be used with the +.Fl s +option. +.It Fl R +Determine a set of commits to display as usual, but display these commits +in reverse order. +.It Fl r Ar repository-path +Use the repository at the specified path. +If not specified, assume the repository is located at or above the current +working directory. +If this directory is a +.Nm +work tree, use the repository path associated with this work tree. +.It Fl S Ar search-pattern +If specified, show only commits with a log message, author name, +committer name, or ID SHA1 hash matched by the extended regular +expression +.Ar search-pattern . +Lines in committed patches will be matched if +.Fl p +is specified. +File paths changed by a commit will be matched if +.Fl P +is specified. +Regular expression syntax is documented in +.Xr re_format 7 . +.It Fl s +Display a short one-line summary of each commit, instead of the default +history format. +Cannot be used together with the +.Fl p +or +.Fl P +option. +.It Fl x Ar commit +Stop traversing commit history immediately after the specified +.Ar commit +has been traversed. +This option has no effect if the specified +.Ar commit +is never traversed. +.El +.Tg di +.It Xo +.Cm diff +.Op Fl adPsw +.Op Fl C Ar number +.Op Fl c Ar commit +.Op Fl r Ar repository-path +.Op Ar object1 Ar object2 | Ar path ... +.Xc +.Dl Pq alias: Cm di +When invoked within a work tree without any arguments, display all +local changes in the work tree. +If one or more +.Ar path +arguments are specified, only show changes within the specified paths. +.Pp +If two arguments are provided, treat each argument as a reference, a tag +name, or an object ID SHA1 hash, and display differences between the +corresponding objects. +Both objects must be of the same type (blobs, trees, or commits). +An abbreviated hash argument will be expanded to a full SHA1 hash +automatically, provided the abbreviation is unique. +If none of these interpretations produce a valid result or if the +.Fl P +option is used, +and if +.Cm got diff +is running in a work tree, attempt to interpret the two arguments as paths. +.Pp +The options for +.Cm got diff +are as follows: +.Bl -tag -width Ds +.It Fl a +Treat file contents as ASCII text even if binary data is detected. +.It Fl C Ar number +Set the number of context lines shown in the diff. +By default, 3 lines of context are shown. +.It Fl c Ar commit +Show differences between commits in the repository. +This option may be used up to two times. +When used only once, show differences between the specified +.Ar commit +and its first parent commit. +When used twice, show differences between the two specified commits. +.Pp +The expected argument is a commit ID SHA1 hash or an existing reference +or tag name which will be resolved to a commit ID. +An abbreviated hash argument will be expanded to a full SHA1 hash +automatically, provided the abbreviation is unique. +.Pp +If the +.Fl c +option is used, all non-option arguments will be interpreted as paths. +If one or more such +.Ar path +arguments are provided, only show differences for the specified paths. +.Pp +Cannot be used together with the +.Fl P +option. +.It Fl d +Display diffstat of changes before the actual diff by annotating each file path +or blob hash being diffed with the total number of lines added and removed. +A summary line will display the total number of changes across all files. +.It Fl P +Interpret all arguments as paths only. +This option can be used to resolve ambiguity in cases where paths +look like tag names, reference names, or object IDs. +This option is only valid when +.Cm got diff +is invoked in a work tree. +.It Fl r Ar repository-path +Use the repository at the specified path. +If not specified, assume the repository is located at or above the current +working directory. +If this directory is a +.Nm +work tree, use the repository path associated with this work tree. +.It Fl s +Show changes staged with +.Cm got stage +instead of showing local changes in the work tree. +This option is only valid when +.Cm got diff +is invoked in a work tree. +.It Fl w +Ignore whitespace-only changes. +.El +.Tg bl +.It Xo +.Cm blame +.Op Fl c Ar commit +.Op Fl r Ar repository-path +.Ar path +.Xc +.Dl Pq alias: Cm bl +Display line-by-line history of a file at the specified path. +.Pp +The options for +.Cm got blame +are as follows: +.Bl -tag -width Ds +.It Fl c Ar commit +Start traversing history at the specified +.Ar commit . +The expected argument is a commit ID SHA1 hash or an existing reference +or tag name which will be resolved to a commit ID. +An abbreviated hash argument will be expanded to a full SHA1 hash +automatically, provided the abbreviation is unique. +.It Fl r Ar repository-path +Use the repository at the specified path. +If not specified, assume the repository is located at or above the current +working directory. +If this directory is a +.Nm +work tree, use the repository path associated with this work tree. +.El +.Tg tr +.It Xo +.Cm tree +.Op Fl iR +.Op Fl c Ar commit +.Op Fl r Ar repository-path +.Op Ar path +.Xc +.Dl Pq alias: Cm tr +Display a listing of files and directories at the specified +directory path in the repository. +Entries shown in this listing may carry one of the following trailing +annotations: +.Bl -column YXZ description +.It @ Ta entry is a symbolic link +.It / Ta entry is a directory +.It * Ta entry is an executable file +.It $ Ta entry is a Git submodule +.El +.Pp +Symbolic link entries are also annotated with the target path of the link. +.Pp +If no +.Ar path +is specified, list the repository path corresponding to the current +directory of the work tree, or the root directory of the repository +if there is no work tree. +.Pp +The options for +.Cm got tree +are as follows: +.Bl -tag -width Ds +.It Fl c Ar commit +List files and directories as they appear in the specified +.Ar commit . +The expected argument is a commit ID SHA1 hash or an existing reference +or tag name which will be resolved to a commit ID. +An abbreviated hash argument will be expanded to a full SHA1 hash +automatically, provided the abbreviation is unique. +.It Fl i +Show object IDs of files (blob objects) and directories (tree objects). +.It Fl R +Recurse into sub-directories in the repository. +.It Fl r Ar repository-path +Use the repository at the specified path. +If not specified, assume the repository is located at or above the current +working directory. +If this directory is a +.Nm +work tree, use the repository path associated with this work tree. +.El +.It Xo +.Cm ref +.Op Fl dlt +.Op Fl c Ar object +.Op Fl r Ar repository-path +.Op Fl s Ar reference +.Op Ar name +.Xc +Manage references in a repository. +.Pp +References may be listed, created, deleted, and changed. +When creating, deleting, or changing a reference the specified +.Ar name +must be an absolute reference name, i.e. it must begin with +.Dq refs/ . +.Pp +The options for +.Cm got ref +are as follows: +.Bl -tag -width Ds +.It Fl c Ar object +Create a reference or change an existing reference. +The reference with the specified +.Ar name +will point at the specified +.Ar object . +The expected +.Ar object +argument is a ID SHA1 hash or an existing reference or tag name which will +be resolved to the ID of a corresponding commit, tree, tag, or blob object. +Cannot be used together with any other options except +.Fl r . +.It Fl d +Delete the reference with the specified +.Ar name +from the repository. +Any commit, tree, tag, and blob objects belonging to deleted references +remain in the repository and may be removed separately with +Git's garbage collector or +.Cm gotadmin cleanup . +Cannot be used together with any other options except +.Fl r . +.It Fl l +List references in the repository. +If no +.Ar name +is specified, list all existing references in the repository. +If +.Ar name +is a reference namespace, list all references in this namespace. +Otherwise, show only the reference with the given +.Ar name . +Cannot be used together with any other options except +.Fl r +and +.Fl t . +.It Fl r Ar repository-path +Use the repository at the specified path. +If not specified, assume the repository is located at or above the current +working directory. +If this directory is a +.Nm +work tree, use the repository path associated with this work tree. +.It Fl s Ar reference +Create a symbolic reference, or change an existing symbolic reference. +The symbolic reference with the specified +.Ar name +will point at the specified +.Ar reference +which must already exist in the repository. +Care should be taken not to create loops between references when +this option is used. +Cannot be used together with any other options except +.Fl r . +.It Fl t +Sort listed references by modification time (most recently modified first) +instead of sorting by lexicographical order. +Use of this option requires the +.Fl l +option to be used as well. +.El +.Tg br +.It Xo +.Cm branch +.Op Fl lnt +.Op Fl c Ar commit +.Op Fl d Ar name +.Op Fl r Ar repository-path +.Op Ar name +.Xc +.Dl Pq alias: Cm br +Create, list, or delete branches. +.Pp +Local branches are managed via references which live in the +.Dq refs/heads/ +reference namespace. +The +.Cm got branch +command creates references in this namespace only. +.Pp +When deleting branches, the specified +.Ar name +is searched in the +.Dq refs/heads +reference namespace first. +If no corresponding branch is found, the +.Dq refs/remotes +namespace will be searched next. +.Pp +If invoked in a work tree without any arguments, print the name of the +work tree's current branch. +.Pp +If a +.Ar name +argument is passed, attempt to create a branch reference with the given name. +By default the new branch reference will point at the latest commit on the +work tree's current branch if invoked in a work tree, and otherwise to a commit +resolved via the repository's HEAD reference. +.Pp +If invoked in a work tree, once the branch was created successfully +switch the work tree's head reference to the newly created branch and +update files across the entire work tree, just like +.Cm got update -b Ar name +would do. +Show the status of each affected file, using the following status codes: +.Bl -column YXZ description +.It U Ta file was updated and contained no local changes +.It G Ta file was updated and local changes were merged cleanly +.It C Ta file was updated and conflicts occurred during merge +.It D Ta file was deleted +.It A Ta new file was added +.It \(a~ Ta versioned file is obstructed by a non-regular file +.It ! Ta a missing versioned file was restored +.El +.Pp +The options for +.Cm got branch +are as follows: +.Bl -tag -width Ds +.It Fl c Ar commit +Make a newly created branch reference point at the specified +.Ar commit . +The expected +.Ar commit +argument is a commit ID SHA1 hash or an existing reference +or tag name which will be resolved to a commit ID. +.It Fl d Ar name +Delete the branch with the specified +.Ar name +from the +.Dq refs/heads +or +.Dq refs/remotes +reference namespace. +.Pp +Only the branch reference is deleted. +Any commit, tree, and blob objects belonging to the branch +remain in the repository and may be removed separately with +Git's garbage collector or +.Cm gotadmin cleanup . +.It Fl l +List all existing branches in the repository, including copies of remote +repositories' branches in the +.Dq refs/remotes/ +reference namespace. +.Pp +If invoked in a work tree, the work tree's current branch is shown +with one of the following annotations: +.Bl -column YXZ description +.It * Ta work tree's base commit matches the branch tip +.It \(a~ Ta work tree's base commit is out-of-date +.El +.It Fl n +Do not switch and update the work tree after creating a new branch. +.It Fl r Ar repository-path +Use the repository at the specified path. +If not specified, assume the repository is located at or above the current +working directory. +If this directory is a +.Nm +work tree, use the repository path associated with this work tree. +.It Fl t +Sort listed branches by modification time (most recently modified first) +instead of sorting by lexicographical order. +Branches in the +.Dq refs/heads/ +reference namespace are listed before branches in +.Dq refs/remotes/ +regardless. +Use of this option requires the +.Fl l +option to be used as well. +.El +.It Xo +.Cm tag +.Op Fl lVv +.Op Fl c Ar commit +.Op Fl m Ar message +.Op Fl r Ar repository-path +.Op Fl s Ar signer-id +.Ar name +.Xc +Manage tags in a repository. +.Pp +Tags are managed via references which live in the +.Dq refs/tags/ +reference namespace. +The +.Cm got tag +command operates on references in this namespace only. +References in this namespace point at tag objects which contain a pointer +to another object, a tag message, as well as author and timestamp information. +.Pp +Attempt to create a tag with the given +.Ar name , +and make this tag point at the given +.Ar commit . +If no commit is specified, default to the latest commit on the work tree's +current branch if invoked in a work tree, and to a commit resolved via +the repository's HEAD reference otherwise. +.Pp +The options for +.Cm got tag +are as follows: +.Bl -tag -width Ds +.It Fl c Ar commit +Make the newly created tag reference point at the specified +.Ar commit . +The expected +.Ar commit +argument is a commit ID SHA1 hash or an existing reference or tag name which +will be resolved to a commit ID. +An abbreviated hash argument will be expanded to a full SHA1 hash +automatically, provided the abbreviation is unique. +.It Fl l +List all existing tags in the repository instead of creating a new tag. +If a +.Ar name +argument is passed, show only the tag with the given +.Ar name . +.It Fl m Ar message +Use the specified tag message when creating the new tag. +Without the +.Fl m +option, +.Cm got tag +opens a temporary file in an editor where a tag message can be written. +.It Fl r Ar repository-path +Use the repository at the specified path. +If not specified, assume the repository is located at or above the current +working directory. +If this directory is a +.Nm +work tree, use the repository path associated with this work tree. +.It Fl s Ar signer-id +While creating a new tag, sign this tag with the identity given in +.Ar signer-id . +.Pp +For SSH-based signatures, +.Ar signer-id +is the path to a file which may refer to either a private SSH key, +or a public SSH key with the private half available via +.Xr ssh-agent 1 . +.Cm got tag +will sign the tag object by invoking +.Xr ssh-keygen 1 +with the +.Fl Y Cm sign +command, using the signature namespace +.Dq git +for compatibility with +.Xr git 1 . +.It Fl V +Verify tag object signatures. +If a +.Ar name +is specified, show and verify the tag object with the provided name. +Otherwise, list all tag objects and verify signatures where present. +.Pp +.Cm got tag +verifies SSH-based signatures by invoking +.Xr ssh-keygen 1 +with the options +.Fl Y Cm verify Fl f Ar allowed_signers . +A path to the +.Ar allowed_signers +file must be set in +.Xr got.conf 5 , +otherwise verification is impossible. +.It Fl v +Verbose mode. +During SSH signature creation and verification this option will be passed to +.Xr ssh-keygen 1 . +Multiple -v options increase the verbosity. +The maximum is 3. +.El +.Pp +By design, the +.Cm got tag +command will not delete tags or change existing tags. +If a tag must be deleted, the +.Cm got ref +command may be used to delete a tag's reference. +This should only be done if the tag has not already been copied to +another repository. +.It Xo +.Cm add +.Op Fl IR +.Ar path ... +.Xc +Schedule unversioned files in a work tree for addition to the +repository in the next commit. +By default, files which match a +.Cm got status +ignore pattern will not be added. +.Pp +If a +.Ar path +mentioned in the command line is not an unversioned file then +.Cm got add +may raise an error. +To avoid unnecessary errors from paths picked up by file globbing patterns +in the shell, paths in the argument list will be silently ignored if they +are not reported by +.Cm got status +at all, or if they are reported with one of the following status codes +and do not have changes staged via +.Cm got stage : +.Bl -column YXZ description +.It M Ta modified file +.It A Ta file scheduled for addition in next commit +.It C Ta modified or added file which contains merge conflicts +.It m Ta modified file modes (executable bit only) +.El +.Pp +The options for +.Cm got add +are as follows: +.Bl -tag -width Ds +.It Fl I +Add files even if they match a +.Cm got status +ignore pattern. +.It Fl R +Permit recursion into directories. +If this option is not specified, +.Cm got add +will refuse to run if a specified +.Ar path +is a directory. +.El +.Tg rm +.It Xo +.Cm remove +.Op Fl fkR +.Op Fl s Ar status-codes +.Ar path ... +.Xc +.Dl Pq alias: Cm rm +Remove versioned files from a work tree and schedule them for deletion +from the repository in the next commit. +.Pp +The options for +.Cm got remove +are as follows: +.Bl -tag -width Ds +.It Fl f +Perform the operation even if a file contains local modifications, +and do not raise an error if a specified +.Ar path +does not exist on disk. +.It Fl k +Keep affected files on disk. +.It Fl R +Permit recursion into directories. +If this option is not specified, +.Cm got remove +will refuse to run if a specified +.Ar path +is a directory. +.It Fl s Ar status-codes +Only delete files with a modification status matching one of the +single-character status codes contained in the +.Ar status-codes +argument. +The following status codes may be specified: +.Bl -column YXZ description +.It M Ta modified file (this implies the +.Fl f +option) +.It ! Ta versioned file expected on disk but missing +.El +.El +.Tg pa +.It Xo +.Cm patch +.Op Fl nR +.Op Fl c Ar commit +.Op Fl p Ar strip-count +.Op Ar patchfile +.Xc +.Dl Pq alias: Cm pa +Apply changes from +.Ar patchfile +to files in a work tree. +Files added or removed by a patch will be scheduled for addition or removal in +the work tree. +.Pp +The patch must be in the unified diff format as produced by +.Cm got diff , +.Xr git-diff 1 , +or by +.Xr diff 1 +and +.Xr cvs 1 +diff when invoked with their +.Fl u +options. +If no +.Ar patchfile +argument is provided, read unified diff data from standard input instead. +.Pp +If the +.Ar patchfile +contains multiple patches, then attempt to apply each of them in sequence. +.Pp +Show the status of each affected file, using the following status codes: +.Bl -column XYZ description +.It M Ta file was modified +.It G Ta file was merged using a merge-base found in the repository +.It C Ta file was merged and conflicts occurred during merge +.It D Ta file was deleted +.It A Ta file was added +.It # Ta failed to patch the file +.El +.Pp +If a change does not match at its exact line number, attempt to +apply it somewhere else in the file if a good spot can be found. +Otherwise, the patch will fail to apply. +.Pp +.Nm +.Cm patch +will refuse to apply a patch if certain preconditions are not met. +Files to be deleted must already be under version control, and must +not have been scheduled for deletion already. +Files to be added must not yet be under version control and must not +already be present on disk. +Files to be modified must already be under version control and may not +contain conflict markers. +.Pp +If an error occurs, the +.Cm patch +operation will be aborted. +Any changes made to the work tree up to this point will be left behind. +Such changes can be viewed with +.Cm got diff +and can be reverted with +.Cm got revert +if needed. +.Pp +The options for +.Cm got patch +are as follows: +.Bl -tag -width Ds +.It Fl c Ar commit +Attempt to locate files within the specified +.Ar commit +for use as a merge-base for 3-way merges. +Ideally, the specified +.Ar commit +should contain versions of files which the changes contained in the +.Ar patchfile +were based on. +Files will be located by path, relative to the repository root. +If the +.Fl p +option is used then leading path components will be stripped +before paths are looked up in the repository. +.Pp +If the +.Fl c +option is not used then +.Cm got patch +will attempt to locate merge-bases via object IDs found in +.Ar patchfile +meta-data, such as produced by +.Cm got diff +or +.Xr git-diff 1 . +Use of the +.Fl c +option is only recommended in the absence of such meta-data. +.Pp +In case no merge-base is available for a file, changes will be applied +without doing a 3-way merge. +Changes which do not apply cleanly may then be rejected entirely, rather +than producing merge conflicts in the patched target file. +.It Fl n +Do not make any modifications to the work tree. +This can be used to check whether a patch would apply without issues. +If the +.Ar patchfile +contains diffs that affect the same file multiple times, the results +displayed may be incorrect. +.It Fl p Ar strip-count +Specify the number of leading path components to strip from paths +parsed from +.Ar patchfile . +If the +.Fl p +option is not used, +.Sq a/ +and +.Sq b/ +path prefixes generated by +.Xr git-diff 1 +will be recognized and stripped automatically. +.It Fl R +Reverse the patch before applying it. +.El +.Tg rv +.It Xo +.Cm revert +.Op Fl pR +.Op Fl F Ar response-script +.Ar path ... +.Xc +.Dl Pq alias: Cm rv +Revert any local changes in files at the specified paths in a work tree. +File contents will be overwritten with those contained in the +work tree's base commit. +There is no way to bring discarded changes back after +.Cm got revert ! +.Pp +If a file was added with +.Cm got add , +it will become an unversioned file again. +If a file was deleted with +.Cm got remove , +it will be restored. +.Pp +The options for +.Cm got revert +are as follows: +.Bl -tag -width Ds +.It Fl F Ar response-script +With the +.Fl p +option, read +.Dq y , +.Dq n , +and +.Dq q +responses line-by-line from the specified +.Ar response-script +file instead of prompting interactively. +.It Fl p +Instead of reverting all changes in files, interactively select or reject +changes to revert based on +.Dq y +(revert change), +.Dq n +(keep change), and +.Dq q +(quit reverting this file) responses. +If a file is in modified status, individual patches derived from the +modified file content can be reverted. +Files in added or deleted status may only be reverted in their entirety. +.It Fl R +Permit recursion into directories. +If this option is not specified, +.Cm got revert +will refuse to run if a specified +.Ar path +is a directory. +.El +.Tg ci +.It Xo +.Cm commit +.Op Fl CNnS +.Op Fl A Ar author +.Op Fl F Ar path +.Op Fl m Ar message +.Op Ar path ... +.Xc +.Dl Pq alias: Cm ci +Create a new commit in the repository from changes in a work tree +and use this commit as the new base commit for the work tree. +If no +.Ar path +is specified, commit all changes in the work tree. +Otherwise, commit changes at or within the specified paths. +.Pp +If changes have been explicitly staged for commit with +.Cm got stage , +only commit staged changes and reject any specified paths which +have not been staged. +.Pp +.Cm got commit +opens a temporary file in an editor where a log message can be written +unless the +.Fl m +option is used +or the +.Fl F +and +.Fl N +options are used together. +.Pp +Show the status of each affected file, using the following status codes: +.Bl -column YXZ description +.It M Ta modified file +.It D Ta file was deleted +.It A Ta new file was added +.It m Ta modified file modes (executable bit only) +.El +.Pp +Files which are not part of the new commit will retain their previously +recorded base commit. +Some +.Nm +commands may refuse to run while the work tree contains files from +multiple base commits. +The base commit of such a work tree can be made consistent by running +.Cm got update +across the entire work tree. +.Pp +The +.Cm got commit +command requires the +.Ev GOT_AUTHOR +environment variable to be set, +unless an author has been configured in +.Xr got.conf 5 +or Git's +.Dv user.name +and +.Dv user.email +configuration settings can be +obtained from the repository's +.Pa .git/config +file or from Git's global +.Pa ~/.gitconfig +configuration file. +.Pp +The options for +.Cm got commit +are as follows: +.Bl -tag -width Ds +.It Fl A Ar author +Set author information in the newly created commit to +.Ar author . +This is useful when committing changes on behalf of someone else. +The +.Ar author +argument must use the same format as the +.Ev GOT_AUTHOR +environment variable. +.Pp +In addition to storing author information, the newly created commit +object will retain +.Dq committer +information which is obtained, as usual, from the +.Ev GOT_AUTHOR +environment variable, or +.Xr got.conf 5 , +or Git configuration settings. +.It Fl C +Allow committing files in conflicted status. +.Pp +Committing files with conflict markers should generally be avoided. +Cases where conflict markers must be stored in the repository for +some legitimate reason should be very rare. +There are usually ways to avoid storing conflict markers verbatim by +applying appropriate programming tricks. +.It Fl F Ar path +Use the prepared log message stored in the file found at +.Ar path +when creating the new commit. +.Cm got commit +opens a temporary file in an editor where the prepared log message can be +reviewed and edited further if needed. +Cannot be used together with the +.Fl m +option. +.It Fl m Ar message +Use the specified log message when creating the new commit. +Cannot be used together with the +.Fl F +option. +.It Fl N +This option prevents +.Cm got commit +from opening the commit message in an editor. +It has no effect unless it is used together with the +.Fl F +option and is intended for non-interactive use such as scripting. +.It Fl n +This option prevents +.Cm got commit +from generating a diff of the to-be-committed changes in a temporary file +which can be viewed while editing a commit message. +.It Fl S +Allow the addition of symbolic links which point outside of the path space +that is under version control. +By default, +.Cm got commit +will reject such symbolic links due to safety concerns. +As a precaution, +.Nm +may decide to represent such a symbolic link as a regular file which contains +the link's target path, rather than creating an actual symbolic link which +points outside of the work tree. +Use of this option is discouraged because external mechanisms such as +.Dq make obj +are better suited for managing symbolic links to paths not under +version control. +.El +.Pp +.Cm got commit +will refuse to run if certain preconditions are not met. +If the work tree's current branch is not in the +.Dq refs/heads/ +reference namespace, new commits may not be created on this branch. +Local changes may only be committed if they are based on file content +found in the most recent commit on the work tree's branch. +If a path is found to be out of date, +.Cm got update +must be used first in order to merge local changes with changes made +in the repository. +.Tg se +.It Xo +.Cm send +.Op Fl afqTv +.Op Fl b Ar branch +.Op Fl d Ar branch +.Op Fl r Ar repository-path +.Op Fl t Ar tag +.Op Ar remote-repository +.Xc +.Dl Pq alias: Cm se +Send new changes to a remote repository. +If no +.Ar remote-repository +is specified, +.Dq origin +will be used. +The remote repository's URL is obtained from the corresponding entry in +.Xr got.conf 5 +or Git's +.Pa config +file of the local repository, as created by +.Cm got clone . +.Pp +All objects corresponding to new changes will be written to a temporary +pack file which is then uploaded to the server. +Upon success, references in the +.Dq refs/remotes/ +reference namespace of the local repository will be updated to point at +the commits which have been sent. +.Pp +By default, changes will only be sent if they are based on up-to-date +copies of relevant branches in the remote repository. +If any changes to be sent are based on out-of-date copies or would +otherwise break linear history of existing branches, new changes must +be fetched from the server with +.Cm got fetch +and local branches must be rebased with +.Cm got rebase +before +.Cm got send +can succeed. +The +.Fl f +option can be used to make exceptions to these requirements. +.Pp +The options for +.Cm got send +are as follows: +.Bl -tag -width Ds +.It Fl a +Send all branches from the local repository's +.Dq refs/heads/ +reference namespace. +The +.Fl a +option is equivalent to listing all branches with multiple +.Fl b +options. +Cannot be used together with the +.Fl b +option. +.It Fl b Ar branch +Send the specified +.Ar branch +from the local repository's +.Dq refs/heads/ +reference namespace. +This option may be specified multiple times to build a list of branches +to send. +If this option is not specified, default to the work tree's current branch +if invoked in a work tree, or to the repository's HEAD reference. +Cannot be used together with the +.Fl a +option. +.It Fl d Ar branch +Delete the specified +.Ar branch +from the remote repository's +.Dq refs/heads/ +reference namespace. +This option may be specified multiple times to build a list of branches +to delete. +.Pp +Only references are deleted. +Any commit, tree, tag, and blob objects belonging to deleted branches +may become subject to deletion by Git's garbage collector running on +the server. +.Pp +Requesting deletion of branches results in an error if the server +does not support this feature or disallows the deletion of branches +based on its configuration. +.It Fl f +Attempt to force the server to overwrite existing branches or tags +in the remote repository, even when +.Cm got fetch +followed by +.Cm got rebase +or +.Cm got merge +would usually be required before changes can be sent. +The server may reject forced requests regardless, depending on its +configuration. +.Pp +Any commit, tree, tag, and blob objects belonging to overwritten branches +or tags may become subject to deletion by Git's garbage collector running +on the server. +.Pp +The +.Dq refs/tags +reference namespace is globally shared between all repositories. +Use of the +.Fl f +option to overwrite tags is discouraged because it can lead to +inconsistencies between the tags present in different repositories. +In general, creating a new tag with a different name is recommended +instead of overwriting an existing tag. +.Pp +Use of the +.Fl f +option is particularly discouraged if changes being sent are based +on an out-of-date copy of a branch in the remote repository. +Instead of using the +.Fl f +option, new changes should +be fetched with +.Cm got fetch +and local branches should be rebased with +.Cm got rebase +or merged with +.Cm got merge , +followed by another attempt to send the changes. +.Pp +The +.Fl f +option should only be needed in situations where the remote repository's +copy of a branch or tag is known to be out-of-date and is considered +disposable. +The risks of creating inconsistencies between different repositories +should also be taken into account. +.It Fl q +Suppress progress reporting output. +The same option will be passed to +.Xr ssh 1 +if applicable. +.It Fl r Ar repository-path +Use the repository at the specified path. +If not specified, assume the repository is located at or above the current +working directory. +If this directory is a +.Nm +work tree, use the repository path associated with this work tree. +.It Fl T +Attempt to send all tags from the local repository's +.Dq refs/tags/ +reference namespace. +The +.Fl T +option is equivalent to listing all tags with multiple +.Fl t +options. +Cannot be used together with the +.Fl t +option. +.It Fl t Ar tag +Send the specified +.Ar tag +from the local repository's +.Dq refs/tags/ +reference namespace, in addition to any branches that are being sent. +The +.Fl t +option may be specified multiple times to build a list of tags to send. +No tags will be sent if the +.Fl t +option is not used. +.Pp +Raise an error if the specified +.Ar tag +already exists in the remote repository, unless the +.Fl f +option is used to overwrite the server's copy of the tag. +In general, creating a new tag with a different name is recommended +instead of overwriting an existing tag. +.Pp +Cannot be used together with the +.Fl T +option. +.It Fl v +Verbose mode. +Causes +.Cm got send +to print debugging messages to standard error output. +The same option will be passed to +.Xr ssh 1 +if applicable. +Multiple -v options increase the verbosity. +The maximum is 3. +.El +.Tg cy +.It Xo +.Cm cherrypick +.Op Fl lX +.Op Ar commit +.Xc +.Dl Pq alias: Cm cy +Merge changes from a single +.Ar commit +into the work tree. +The specified +.Ar commit +should be on a different branch than the work tree's base commit. +The expected argument is a reference or a commit ID SHA1 hash. +An abbreviated hash argument will be expanded to a full SHA1 hash +automatically, provided the abbreviation is unique. +.Pp +Show the status of each affected file, using the following status codes: +.Bl -column YXZ description +.It G Ta file was merged +.It C Ta file was merged and conflicts occurred during merge +.It ! Ta changes destined for a missing file were not merged +.It D Ta file was deleted +.It d Ta file's deletion was prevented by local modifications +.It A Ta new file was added +.It \(a~ Ta changes destined for a non-regular file were not merged +.It ? Ta changes destined for an unversioned file were not merged +.El +.Pp +The merged changes will appear as local changes in the work tree, which +may be viewed with +.Cm got diff , +amended manually or with further +.Cm got cherrypick +commands, +committed with +.Cm got commit . +.Pp +If invoked in a work tree where no +.Cm rebase , +.Cm histedit , +or +.Cm merge +operation is taking place, +.Cm got cherrypick +creates a record of commits which have been merged into the work tree. +When a file changed by +.Cm got cherrypick +is committed with +.Cm got commit , +the log messages of relevant merged commits will then appear in the editor, +where the messages should be further adjusted to convey the reasons for +cherrypicking the changes. +Upon exiting the editor, if the time stamp of the log message file +is unchanged or the log message is empty, +.Cm got commit +will fail with an unmodified or empty log message error. +.Pp +If all the changes in all files touched by a given commit are discarded, +e.g. with +.Cm got revert , +this commit's log message record will also disappear. +.Pp +.Cm got cherrypick +will refuse to run if certain preconditions are not met. +If the work tree contains multiple base commits, it must first be updated +to a single base commit with +.Cm got update . +If any relevant files already contain merge conflicts, these +conflicts must be resolved first. +.Pp +The options for +.Nm +.Cm cherrypick +are as follows: +.Bl -tag -width Ds +.It Fl l +Display a list of commit log messages recorded by cherrypick operations, +represented by references in the +.Dq refs/got/worktree +reference namespace. +If a +.Ar commit +is specified, only show the log message of the specified commit. +.Pp +If invoked in a work tree, only log messages recorded by cherrypick operations +in the current work tree will be displayed. +Otherwise, all commit log messages will be displayed irrespective of the +work tree in which they were created. +This option cannot be used with +.Fl X . +.It Fl X +Delete log messages created by previous cherrypick operations, represented by +references in the +.Dq refs/got/worktree +reference namespace. +If a +.Ar commit +is specified, only delete the log message of the specified commit. +.Pp +If invoked in a work tree, only log messages recorded by cherrypick operations +in the current work tree will be deleted. +Otherwise, all commit log messages will be deleted irrespective of the +work tree in which they were created. +This option cannot be used with +.Fl l . +.El +.Pp +.Tg bo +.It Xo +.Cm backout +.Op Fl lX +.Op Ar commit +.Xc +.Dl Pq alias: Cm bo +Reverse-merge changes from a single +.Ar commit +into the work tree. +The specified +.Ar commit +should be on the same branch as the work tree's base commit. +The expected argument is a reference or a commit ID SHA1 hash. +An abbreviated hash argument will be expanded to a full SHA1 hash +automatically, provided the abbreviation is unique. +.Pp +Show the status of each affected file, using the following status codes: +.Bl -column YXZ description +.It G Ta file was merged +.It C Ta file was merged and conflicts occurred during merge +.It ! Ta changes destined for a missing file were not merged +.It D Ta file was deleted +.It d Ta file's deletion was prevented by local modifications +.It A Ta new file was added +.It \(a~ Ta changes destined for a non-regular file were not merged +.It ? Ta changes destined for an unversioned file were not merged +.El +.Pp +The reverse-merged changes will appear as local changes in the work tree, +which may be viewed with +.Cm got diff , +amended manually or with further +.Cm got backout +commands, +committed with +.Cm got commit . +.Pp +If invoked in a work tree where no +.Cm rebase , +.Cm histedit , +or +.Cm merge +operation is taking place, +.Cm got backout +creates a record of commits which have been reverse-merged into the work tree. +When a file changed by +.Cm got backout +is committed with +.Cm got commit , +the log messages of relevant reverse-merged commits will then appear in +the editor, where the messages should be further adjusted to convey the +reasons for backing out the changes. +Upon exiting the editor, if the time stamp of the log message file +is unchanged or the log message is empty, +.Cm got commit +will fail with an unmodified or empty log message error. +.Pp +If all the changes in all files touched by a given commit are discarded, +e.g. with +.Cm got revert , +this commit's log message record will also disappear. +.Pp +.Cm got backout +will refuse to run if certain preconditions are not met. +If the work tree contains multiple base commits, it must first be updated +to a single base commit with +.Cm got update . +If any relevant files already contain merge conflicts, these +conflicts must be resolved first. +.Pp +The options for +.Nm +.Cm backout +are as follows: +.Bl -tag -width Ds +.It Fl l +Display a list of commit log messages recorded by backout operations, +represented by references in the +.Dq refs/got/worktree +reference namespace. +If a +.Ar commit +is specified, only show the log message of the specified commit. +.Pp +If invoked in a work tree, only log messages recorded by backout operations +in the current work tree will be displayed. +Otherwise, all commit log messages will be displayed irrespective of the +work tree in which they were created. +This option cannot be used with +.Fl X . +.It Fl X +Delete log messages created by previous backout operations, represented by +references in the +.Dq refs/got/worktree +reference namespace. +If a +.Ar commit +is specified, only delete the log message of the specified commit. +.Pp +If invoked in a work tree, only log messages recorded by backout operations +in the current work tree will be deleted. +Otherwise, all commit log messages will be deleted irrespective of the +work tree in which they were created. +This option cannot be used with +.Fl l . +.El +.Pp +.Tg rb +.It Xo +.Cm rebase +.Op Fl aCclX +.Op Ar branch +.Xc +.Dl Pq alias: Cm rb +Rebase commits on the specified +.Ar branch +onto the tip of the current branch of the work tree. +The +.Ar branch +must share common ancestry with the work tree's current branch. +Rebasing begins with the first descendant commit of the youngest +common ancestor commit shared by the specified +.Ar branch +and the work tree's current branch, and stops once the tip commit +of the specified +.Ar branch +has been rebased. +.Pp +When +.Cm got rebase +is used as intended, the specified +.Ar branch +represents a local commit history and may already contain changes +that are not yet visible in any other repositories. +The work tree's current branch, which must be set with +.Cm got update -b +before starting the +.Cm rebase +operation, represents a branch from a remote repository which shares +a common history with the specified +.Ar branch +but has progressed, and perhaps diverged, due to commits added to the +remote repository. +.Pp +Rebased commits are accumulated on a temporary branch which the work tree +will remain switched to throughout the entire rebase operation. +Commits on this branch represent the same changes with the same log +messages as their counterparts on the original +.Ar branch , +but with different commit IDs. +Once rebasing has completed successfully, the temporary branch becomes +the new version of the specified +.Ar branch +and the work tree is automatically switched to it. +If author information is available via the +.Ev GOT_AUTHOR +environment variable, +.Xr got.conf 5 +or Git's +.Dv user.name +and +.Dv user.email +configuration settings, this author information will be used to identify +the +.Dq committer +of rebased commits. +.Pp +Old commits in their pre-rebase state are automatically backed up in the +.Dq refs/got/backup/rebase +reference namespace. +As long as these references are not removed older versions of rebased +commits will remain in the repository and can be viewed with the +.Cm got rebase -l +command. +Removal of these references makes objects which become unreachable via +any reference subject to removal by Git's garbage collector or +.Cm gotadmin cleanup . +.Pp +While rebasing commits, show the status of each affected file, +using the following status codes: +.Bl -column YXZ description +.It G Ta file was merged +.It C Ta file was merged and conflicts occurred during merge +.It ! Ta changes destined for a missing file were not merged +.It D Ta file was deleted +.It d Ta file's deletion was prevented by local modifications +.It A Ta new file was added +.It \(a~ Ta changes destined for a non-regular file were not merged +.It ? Ta changes destined for an unversioned file were not merged +.El +.Pp +If merge conflicts occur, the rebase operation is interrupted and may +be continued once conflicts have been resolved. +If any files with destined changes are found to be missing or unversioned, +or if files could not be deleted due to differences in deleted content, +the rebase operation will be interrupted to prevent potentially incomplete +changes from being committed to the repository without user intervention. +The work tree may be modified as desired and the rebase operation can be +continued once the changes present in the work tree are considered complete. +Alternatively, the rebase operation may be aborted which will leave +.Ar branch +unmodified and the work tree switched back to its original branch. +.Pp +If a merge conflict is resolved in a way which renders the merged +change into a no-op change, the corresponding commit will be elided +when the rebase operation continues. +.Pp +.Cm got rebase +will refuse to run if certain preconditions are not met. +If the +.Ar branch +is not in the +.Dq refs/heads/ +reference namespace, the branch may not be rebased. +If the work tree is not yet fully updated to the tip commit of its +branch, then the work tree must first be updated with +.Cm got update . +If changes have been staged with +.Cm got stage , +these changes must first be committed with +.Cm got commit +or unstaged with +.Cm got unstage . +If the work tree contains local changes, these changes must first be +committed with +.Cm got commit +or reverted with +.Cm got revert . +If the +.Ar branch +contains changes to files outside of the work tree's path prefix, +the work tree cannot be used to rebase this branch. +.Pp +The +.Cm got update , +.Cm got integrate , +.Cm got merge , +.Cm got commit , +and +.Cm got histedit +commands will refuse to run while a rebase operation is in progress. +Other commands which manipulate the work tree may be used for +conflict resolution purposes. +.Pp +If the specified +.Ar branch +is already based on the work tree's current branch, then no commits +need to be rebased and +.Cm got rebase +will simply switch the work tree to the specified +.Ar branch +and update files in the work tree accordingly. +.Pp +The options for +.Cm got rebase +are as follows: +.Bl -tag -width Ds +.It Fl a +Abort an interrupted rebase operation. +If this option is used, no other command-line arguments are allowed. +.It Fl C +Allow a rebase operation to continue with files in conflicted status. +This option should generally be avoided, and can only be used with the +.Fl c +option. +.It Fl c +Continue an interrupted rebase operation. +If this option is used, no other command-line arguments are allowed except +.Fl C . +.It Fl l +Show a list of past rebase operations, represented by references in the +.Dq refs/got/backup/rebase +reference namespace. +.Pp +Display the author, date, and log message of each backed up commit, +the object ID of the corresponding post-rebase commit, and +the object ID of their common ancestor commit. +Given these object IDs, +the +.Cm got log +command with the +.Fl c +and +.Fl x +options can be used to examine the history of either version of the branch, +and the +.Cm got branch +command with the +.Fl c +option can be used to create a new branch from a pre-rebase state if desired. +.Pp +If a +.Ar branch +is specified, only show commits which at some point in time represented this +branch. +Otherwise, list all backed up commits for any branches. +.Pp +If this option is used, +.Cm got rebase +does not require a work tree. +None of the other options can be used together with +.Fl l . +.It Fl X +Delete backups created by past rebase operations, represented by references +in the +.Dq refs/got/backup/rebase +reference namespace. +.Pp +If a +.Ar branch +is specified, only delete backups which at some point in time represented +this branch. +Otherwise, delete all references found within +.Dq refs/got/backup/rebase . +.Pp +Any commit, tree, tag, and blob objects belonging to deleted backups +remain in the repository and may be removed separately with +Git's garbage collector or +.Cm gotadmin cleanup . +.Pp +If this option is used, +.Cm got rebase +does not require a work tree. +None of the other options can be used together with +.Fl X . +.El +.Tg he +.It Xo +.Cm histedit +.Op Fl aCcdeflmX +.Op Fl F Ar histedit-script +.Op Ar branch +.Xc +.Dl Pq alias: Cm he +Edit commit history between the work tree's current base commit and +the tip commit of the work tree's current branch. +.Pp +The +.Cm got histedit +command requires the +.Ev GOT_AUTHOR +environment variable to be set, +unless an author has been configured in +.Xr got.conf 5 +or Git's +.Dv user.name +and +.Dv user.email +configuration settings can be obtained from the repository's +.Pa .git/config +file or from Git's global +.Pa ~/.gitconfig +configuration file. +.Pp +Before starting a +.Cm histedit +operation, the work tree's current branch must be set with +.Cm got update -b +to the branch which should be edited, unless this branch is already the +current branch of the work tree. +The tip of this branch represents the upper bound (inclusive) of commits +touched by the +.Cm histedit +operation. +.Pp +Furthermore, the work tree's base commit +must be set with +.Cm got update -c +to a point in this branch's commit history where editing should begin. +This commit represents the lower bound (non-inclusive) of commits touched +by the +.Cm histedit +operation. +.Pp +Editing of commit history is controlled via a +.Ar histedit script +which can be written in an editor based on a template, passed on the +command line, or generated with the +.Fl d , +.Fl e , +.Fl f , +or +.Fl m +options. +.Pp +The format of the histedit script is line-based. +Each line in the script begins with a command name, followed by +whitespace and an argument. +For most commands, the expected argument is a commit ID SHA1 hash. +Any remaining text on the line is ignored. +Lines which begin with the +.Sq # +character are ignored entirely. +.Pp +The available histedit script commands are as follows: +.Bl -column YXZ pick-commit +.It Cm pick Ar commit Ta Use the specified commit as it is. +.It Cm edit Ar commit Ta Apply the changes from the specified commit, but +then interrupt the histedit operation for amending, without creating a commit. +While the histedit operation is interrupted arbitrary files may be edited, +and commands which manipulate the work tree can be used freely. +The +.Cm got add +and +.Cm got remove +commands can be used to add new files or remove existing ones. +The +.Cm got revert -p +command can be used to eliminate arbitrary changes from files in the work tree. +The +.Cm got stage -p +command may be used to prepare a subset of changes for inclusion in the +next commit. +Finally, the +.Cm got commit +command can be used to insert arbitrary commits into the edited history. +Regular editing of history must eventually be resumed by running +.Cm got histedit -c . +.It Cm fold Ar commit Ta Combine the specified commit with the next commit +listed further below that will be used. +.It Cm drop Ar commit Ta Remove this commit from the edited history. +.It Cm mesg Oo Ar log-message Oc Ta Create a new log message for the commit of +a preceding +.Cm pick +or +.Cm edit +command on the previous line of the histedit script. +The optional +.Ar log-message +argument provides a new single-line log message to use. +If the +.Ar log-message +argument is omitted, open an editor where a new log message can be written. +.El +.Pp +Every commit in the history being edited must be mentioned in the script. +Lines may be re-ordered to change the order of commits in the edited history. +No commit may be listed more than once. +.Pp +Edited commits are accumulated on a temporary branch which the work tree +will remain switched to throughout the entire histedit operation. +Once history editing has completed successfully, the temporary branch becomes +the new version of the work tree's branch and the work tree is automatically +switched to it. +.Pp +Old commits in their pre-histedit state are automatically backed up in the +.Dq refs/got/backup/histedit +reference namespace. +As long as these references are not removed older versions of edited +commits will remain in the repository and can be viewed with the +.Cm got histedit -l +command. +Removal of these references makes objects which become unreachable via +any reference subject to removal by Git's garbage collector or +.Cm gotadmin cleanup . +.Pp +While merging commits, show the status of each affected file, +using the following status codes: +.Bl -column YXZ description +.It G Ta file was merged +.It C Ta file was merged and conflicts occurred during merge +.It ! Ta changes destined for a missing file were not merged +.It D Ta file was deleted +.It d Ta file's deletion was prevented by local modifications +.It A Ta new file was added +.It \(a~ Ta changes destined for a non-regular file were not merged +.It ? Ta changes destined for an unversioned file were not merged +.El +.Pp +If merge conflicts occur, the histedit operation is interrupted and may +be continued once conflicts have been resolved. +If any files with destined changes are found to be missing or unversioned, +or if files could not be deleted due to differences in deleted content, +the histedit operation will be interrupted to prevent potentially incomplete +changes from being committed to the repository without user intervention. +The work tree may be modified as desired and the histedit operation can be +continued once the changes present in the work tree are considered complete. +Alternatively, the histedit operation may be aborted which will leave +the work tree switched back to its original branch. +.Pp +If a merge conflict is resolved in a way which renders the merged +change into a no-op change, the corresponding commit will be elided +when the histedit operation continues. +.Pp +.Cm got histedit +will refuse to run if certain preconditions are not met. +If the work tree's current branch is not in the +.Dq refs/heads/ +reference namespace, the history of the branch may not be edited. +If the work tree contains multiple base commits, it must first be updated +to a single base commit with +.Cm got update . +If changes have been staged with +.Cm got stage , +these changes must first be committed with +.Cm got commit +or unstaged with +.Cm got unstage . +If the work tree contains local changes, these changes must first be +committed with +.Cm got commit +or reverted with +.Cm got revert . +If the edited history contains changes to files outside of the work tree's +path prefix, the work tree cannot be used to edit the history of this branch. +.Pp +The +.Cm got update , +.Cm got rebase , +.Cm got merge , +and +.Cm got integrate +commands will refuse to run while a histedit operation is in progress. +Other commands which manipulate the work tree may be used, and the +.Cm got commit +command may be used to commit arbitrary changes to the temporary branch +while the histedit operation is interrupted. +.Pp +The options for +.Cm got histedit +are as follows: +.Bl -tag -width Ds +.It Fl a +Abort an interrupted histedit operation. +If this option is used, no other command-line arguments are allowed. +.It Fl C +Allow a histedit operation to continue with files in conflicted status. +This option should generally be avoided, and can only be used with the +.Fl c +option. +.It Fl c +Continue an interrupted histedit operation. +If this option is used, no other command-line arguments are allowed except +.Fl C . +.It Fl d +Drop all commits. +This option is a quick equivalent to a histedit script which drops all +commits. +The +.Fl d +option can only be used when starting a new histedit operation. +If this option is used, no other command-line arguments are allowed. +.It Fl e +Interrupt the histedit operation for editing after merging each commit. +This option is a quick equivalent to a histedit script which uses the +.Cm edit +command for all commits. +The +.Fl e +option can only be used when starting a new histedit operation. +If this option is used, no other command-line arguments are allowed. +.It Fl F Ar histedit-script +Use the specified +.Ar histedit-script +instead of opening a temporary file in an editor where a histedit script +can be written. +.It Fl f +Fold all commits into a single commit. +This option is a quick equivalent to a histedit script which folds all +commits, combining them all into one commit. +The +.Fl f +option can only be used when starting a new histedit operation. +If this option is used, no other command-line arguments are allowed. +.It Fl l +Show a list of past histedit operations, represented by references in the +.Dq refs/got/backup/histedit +reference namespace. +.Pp +Display the author, date, and log message of each backed up commit, +the object ID of the corresponding post-histedit commit, and +the object ID of their common ancestor commit. +Given these object IDs, +the +.Cm got log +command with the +.Fl c +and +.Fl x +options can be used to examine the history of either version of the branch, +and the +.Cm got branch +command with the +.Fl c +option can be used to create a new branch from a pre-histedit state if desired. +.Pp +If a +.Ar branch +is specified, only show commits which at some point in time represented this +branch. +Otherwise, list all backed up commits for any branches. +.Pp +If this option is used, +.Cm got histedit +does not require a work tree. +None of the other options can be used together with +.Fl l . +.It Fl m +Edit log messages only. +This option is a quick equivalent to a histedit script which edits +only log messages but otherwise leaves every picked commit as-is. +The +.Fl m +option can only be used when starting a new histedit operation. +If this option is used, no other command-line arguments are allowed. +.It Fl X +Delete backups created by past histedit operations, represented by references +in the +.Dq refs/got/backup/histedit +reference namespace. +.Pp +If a +.Ar branch +is specified, only delete backups which at some point in time represented +this branch. +Otherwise, delete all references found within +.Dq refs/got/backup/histedit . +.Pp +Any commit, tree, tag, and blob objects belonging to deleted backups +remain in the repository and may be removed separately with +Git's garbage collector or +.Cm gotadmin cleanup . +.Pp +If this option is used, +.Cm got histedit +does not require a work tree. +None of the other options can be used together with +.Fl X . +.El +.Tg ig +.It Cm integrate Ar branch +.Dl Pq alias: Cm ig +Integrate the specified +.Ar branch +into the work tree's current branch. +Files in the work tree are updated to match the contents on the integrated +.Ar branch , +and the reference of the work tree's branch is changed to point at the +head commit of the integrated +.Ar branch . +.Pp +Both branches can be considered equivalent after integration since they +will be pointing at the same commit. +Both branches remain available for future work, if desired. +In case the integrated +.Ar branch +is no longer needed it may be deleted with +.Cm got branch -d . +.Pp +Show the status of each affected file, using the following status codes: +.Bl -column YXZ description +.It U Ta file was updated +.It D Ta file was deleted +.It A Ta new file was added +.It \(a~ Ta versioned file is obstructed by a non-regular file +.It ! Ta a missing versioned file was restored +.El +.Pp +.Cm got integrate +will refuse to run if certain preconditions are not met. +Most importantly, the +.Ar branch +must have been rebased onto the work tree's current branch with +.Cm got rebase +before it can be integrated, in order to linearize commit history and +resolve merge conflicts. +If the work tree contains multiple base commits, it must first be updated +to a single base commit with +.Cm got update . +If changes have been staged with +.Cm got stage , +these changes must first be committed with +.Cm got commit +or unstaged with +.Cm got unstage . +If the work tree contains local changes, these changes must first be +committed with +.Cm got commit +or reverted with +.Cm got revert . +.Tg mg +.It Xo +.Cm merge +.Op Fl aCcMn +.Op Ar branch +.Xc +.Dl Pq alias: Cm mg +Merge the specified +.Ar branch +into the current branch of the work tree. +If the branches have diverged, creates a merge commit. +Otherwise, if +.Ar branch +already includes all commits from the work tree's branch, updates the work +tree's branch to be the same as +.Ar branch +without creating a commit, and updates the work tree to the most recent commit +on the branch. +.Pp +If a linear project history is desired, then use of +.Cm got rebase +should be preferred over +.Cm got merge . +However, even strictly linear projects may require merge commits in order +to merge in new versions of third-party code stored on vendor branches +created with +.Cm got import . +.Pp +Merge commits are commits based on multiple parent commits. +The tip commit of the work tree's current branch, which must be in the +.Dq refs/heads/ +reference namespace and must be set with +.Cm got update -b +before starting the +.Cm merge +operation, will be used as the first parent. +The tip commit of the specified +.Ar branch +will be used as the second parent. +.Pp +No ancestral relationship between the two branches is required. +If the two branches have already been merged previously, only new changes +will be merged. +.Pp +It is not possible to create merge commits with more than two parents. +If more than one branch needs to be merged, then multiple merge commits +with two parents each can be created in sequence. +.Pp +While merging changes found on the +.Ar branch +into the work tree, show the status of each affected file, +using the following status codes: +.Bl -column YXZ description +.It G Ta file was merged +.It C Ta file was merged and conflicts occurred during merge +.It ! Ta changes destined for a missing file were not merged +.It D Ta file was deleted +.It d Ta file's deletion was prevented by local modifications +.It A Ta new file was added +.It \(a~ Ta changes destined for a non-regular file were not merged +.It ? Ta changes destined for an unversioned file were not merged +.El +.Pp +If merge conflicts occur, the merge operation is interrupted and conflicts +must be resolved before the merge operation can continue. +If any files with destined changes are found to be missing or unversioned, +or if files could not be deleted due to differences in deleted content, +the merge operation will be interrupted to prevent potentially incomplete +changes from being committed to the repository without user intervention. +The work tree may be modified as desired and the merge can be continued +once the changes present in the work tree are considered complete. +Alternatively, the merge operation may be aborted which will leave +the work tree's current branch unmodified. +.Pp +.Cm got merge +will refuse to run if certain preconditions are not met. +If the work tree's current branch is not in the +.Dq refs/heads/ +reference namespace then the work tree must first be switched to a +branch in the +.Dq refs/heads/ +namespace with +.Cm got update -b . +If the work tree is not yet fully updated to the tip commit of its +branch, then the work tree must first be updated with +.Cm got update . +If the work tree contains multiple base commits, it must first be updated +to a single base commit with +.Cm got update . +If changes have been staged with +.Cm got stage , +these changes must first be committed with +.Cm got commit +or unstaged with +.Cm got unstage . +If the work tree contains local changes, these changes must first be +committed with +.Cm got commit +or reverted with +.Cm got revert . +If the +.Ar branch +contains changes to files outside of the work tree's path prefix, +the work tree cannot be used to merge this branch. +.Pp +The +.Cm got update , +.Cm got commit , +.Cm got rebase , +.Cm got histedit , +.Cm got integrate , +and +.Cm got stage +commands will refuse to run while a merge operation is in progress. +Other commands which manipulate the work tree may be used for +conflict resolution purposes. +.Pp +The options for +.Cm got merge +are as follows: +.Bl -tag -width Ds +.It Fl a +Abort an interrupted merge operation. +If this option is used, no other command-line arguments are allowed. +.It Fl C +Allow a merge operation to continue with files in conflicted status. +This option should generally be avoided, and can only be used with the +.Fl c +option. +.It Fl c +Continue an interrupted merge operation. +If this option is used, no other command-line arguments are allowed except +.Fl C . +.It Fl M +Create a merge commit even if the branches have not diverged. +.It Fl n +Merge changes into the work tree as usual but do not create a merge +commit immediately. +The merge result can be adjusted as desired before a merge commit is +created with +.Cm got merge -c . +Alternatively, the merge may be aborted with +.Cm got merge -a . +.El +.Tg sg +.It Xo +.Cm stage +.Op Fl lpS +.Op Fl F Ar response-script +.Op Ar path ... +.Xc +.Dl Pq alias: Cm sg +Stage local changes for inclusion in the next commit. +If no +.Ar path +is specified, stage all changes in the work tree. +Otherwise, stage changes at or within the specified paths. +Paths may be staged if they are added, modified, or deleted according to +.Cm got status . +.Pp +Show the status of each affected file, using the following status codes: +.Bl -column YXZ description +.It A Ta file addition has been staged +.It M Ta file modification has been staged +.It D Ta file deletion has been staged +.El +.Pp +Staged file contents are saved in newly created blob objects in the repository. +These blobs will be referred to by tree objects once staged changes have been +committed. +.Pp +Staged changes affect the behaviour of +.Cm got commit , +.Cm got status , +and +.Cm got diff . +While paths with staged changes exist, the +.Cm got commit +command will refuse to commit any paths which do not have staged changes. +Local changes created on top of staged changes can only be committed if +the path is staged again, or if the staged changes are committed first. +The +.Cm got status +command will show both local changes and staged changes. +The +.Cm got diff +command is able to display local changes relative to staged changes, +and to display staged changes relative to the repository. +The +.Cm got revert +command cannot revert staged changes but may be used to revert +local changes created on top of staged changes. +.Pp +The options for +.Cm got stage +are as follows: +.Bl -tag -width Ds +.It Fl F Ar response-script +With the +.Fl p +option, read +.Dq y , +.Dq n , +and +.Dq q +responses line-by-line from the specified +.Ar response-script +file instead of prompting interactively. +.It Fl l +Instead of staging new changes, list paths which are already staged, +along with the IDs of staged blob objects and stage status codes. +If paths were provided on the command line, show the staged paths +among the specified paths. +Otherwise, show all staged paths. +.It Fl p +Instead of staging the entire content of a changed file, interactively +select or reject changes for staging based on +.Dq y +(stage change), +.Dq n +(reject change), and +.Dq q +(quit staging this file) responses. +If a file is in modified status, individual patches derived from the +modified file content can be staged. +Files in added or deleted status may only be staged or rejected in +their entirety. +.It Fl S +Allow staging of symbolic links which point outside of the path space +that is under version control. +By default, +.Cm got stage +will reject such symbolic links due to safety concerns. +As a precaution, +.Nm +may decide to represent such a symbolic link as a regular file which contains +the link's target path, rather than creating an actual symbolic link which +points outside of the work tree. +Use of this option is discouraged because external mechanisms such as +.Dq make obj +are better suited for managing symbolic links to paths not under +version control. +.El +.Pp +.Cm got stage +will refuse to run if certain preconditions are not met. +If a file contains merge conflicts, these conflicts must be resolved first. +If a file is found to be out of date relative to the head commit on the +work tree's current branch, the file must be updated with +.Cm got update +before it can be staged (however, this does not prevent the file from +becoming out-of-date at some point after having been staged). +.Pp +The +.Cm got update , +.Cm got rebase , +.Cm got merge , +and +.Cm got histedit +commands will refuse to run while staged changes exist. +If staged changes cannot be committed because a staged path +is out of date, the path must be unstaged with +.Cm got unstage +before it can be updated with +.Cm got update , +and may then be staged again if necessary. +.Tg ug +.It Xo +.Cm unstage +.Op Fl p +.Op Fl F Ar response-script +.Op Ar path ... +.Xc +.Dl Pq alias: Cm ug +Merge staged changes back into the work tree and put affected paths +back into non-staged status. +If no +.Ar path +is specified, unstage all staged changes across the entire work tree. +Otherwise, unstage changes at or within the specified paths. +.Pp +Show the status of each affected file, using the following status codes: +.Bl -column YXZ description +.It G Ta file was unstaged +.It C Ta file was unstaged and conflicts occurred during merge +.It ! Ta changes destined for a missing file were not merged +.It D Ta file was staged as deleted and still is deleted +.It d Ta file's deletion was prevented by local modifications +.It \(a~ Ta changes destined for a non-regular file were not merged +.El +.Pp +The options for +.Cm got unstage +are as follows: +.Bl -tag -width Ds +.It Fl F Ar response-script +With the +.Fl p +option, read +.Dq y , +.Dq n , +and +.Dq q +responses line-by-line from the specified +.Ar response-script +file instead of prompting interactively. +.It Fl p +Instead of unstaging the entire content of a changed file, interactively +select or reject changes for unstaging based on +.Dq y +(unstage change), +.Dq n +(keep change staged), and +.Dq q +(quit unstaging this file) responses. +If a file is staged in modified status, individual patches derived from the +staged file content can be unstaged. +Files staged in added or deleted status may only be unstaged in their entirety. +.El +.It Xo +.Cm cat +.Op Fl P +.Op Fl c Ar commit +.Op Fl r Ar repository-path +.Ar arg ... +.Xc +Parse and print contents of objects to standard output in a line-based +text format. +Content of commit, tree, and tag objects is printed in a way similar +to the actual content stored in such objects. +Blob object contents are printed as they would appear in files on disk. +.Pp +Attempt to interpret each argument as a reference, a tag name, or +an object ID SHA1 hash. +References will be resolved to an object ID. +Tag names will resolved to a tag object. +An abbreviated hash argument will be expanded to a full SHA1 hash +automatically, provided the abbreviation is unique. +.Pp +If none of the above interpretations produce a valid result, or if the +.Fl P +option is used, attempt to interpret the argument as a path which will +be resolved to the ID of an object found at this path in the repository. +.Pp +The options for +.Cm got cat +are as follows: +.Bl -tag -width Ds +.It Fl c Ar commit +Look up paths in the specified +.Ar commit . +If this option is not used, paths are looked up in the commit resolved +via the repository's HEAD reference. +The expected argument is a commit ID SHA1 hash or an existing reference +or tag name which will be resolved to a commit ID. +An abbreviated hash argument will be expanded to a full SHA1 hash +automatically, provided the abbreviation is unique. +.It Fl P +Interpret all arguments as paths only. +This option can be used to resolve ambiguity in cases where paths +look like tag names, reference names, or object IDs. +.It Fl r Ar repository-path +Use the repository at the specified path. +If not specified, assume the repository is located at or above the current +working directory. +If this directory is a +.Nm +work tree, use the repository path associated with this work tree. +.El +.It Cm info Op Ar path ... +Display meta-data stored in a work tree. +See +.Xr got-worktree 5 +for details. +.Pp +The work tree to use is resolved implicitly by walking upwards from the +current working directory. +.Pp +If one or more +.Ar path +arguments are specified, show additional per-file information for tracked +files located at or within these paths. +If a +.Ar path +argument corresponds to the work tree's root directory, display information +for all tracked files. +.El +.Sh ENVIRONMENT +.Bl -tag -width GOT_IGNORE_GITCONFIG +.It Ev GOT_AUTHOR +The author's name and email address, such as +.Dq An Flan Hacker Aq Mt flan_hacker@openbsd.org . +Used by the +.Cm got commit , +.Cm got import , +.Cm got rebase , +.Cm got merge , +and +.Cm got histedit +commands. +Because +.Xr git 1 +may fail to parse commits without an email address in author data, +.Nm +attempts to reject +.Ev GOT_AUTHOR +environment variables with a missing email address. +.Pp +.Ev GOT_AUTHOR will be overridden by configuration settings in +.Xr got.conf 5 +or by Git's +.Dv user.name +and +.Dv user.email +configuration settings in the repository's +.Pa .git/config +file. +The +.Dv user.name +and +.Dv user.email +configuration settings contained in Git's global +.Pa ~/.gitconfig +configuration file will only be used if neither +.Xr got.conf 5 +nor the +.Ev GOT_AUTHOR +environment variable provide author information. +.It Ev GOT_IGNORE_GITCONFIG +If this variable is set then any remote repository definitions or author +information found in Git configuration files will be ignored. +.It Ev GOT_LOG_DEFAULT_LIMIT +The default limit on the number of commits traversed by +.Cm got log . +If set to zero, the limit is unbounded. +This variable will be silently ignored if it is set to a non-numeric value. +.It Ev VISUAL , EDITOR +The editor spawned by +.Cm got commit , +.Cm got histedit , +.Cm got import , +or +.Cm got tag . +If not set, the +.Xr vi 1 +text editor will be spawned. +.El +.Sh FILES +.Bl -tag -width packed-refs -compact +.It Pa got.conf +Repository-wide configuration settings for +.Nm . +If present, a +.Xr got.conf 5 +configuration file located in the root directory of a Git repository +supersedes any relevant settings in Git's +.Pa config +file. +.Pp +.It Pa .got/got.conf +Worktree-specific configuration settings for +.Nm . +If present, a +.Xr got.conf 5 +configuration file in the +.Pa .got +meta-data directory of a work tree supersedes any relevant settings in +the repository's +.Xr got.conf 5 +configuration file and Git's +.Pa config +file. +.El +.Sh EXIT STATUS +.Ex -std got +.Sh EXAMPLES +Enable tab-completion of +.Nm +command names in +.Xr ksh 1 : +.Pp +.Dl $ set -A complete_got_1 -- $(got -h 2>&1 | sed -n s/commands://p) +.Pp +Clone an existing Git repository for use with +.Nm : +.Pp +.Dl $ cd /var/git/ +.Dl $ got clone ssh://git@github.com/openbsd/src.git +.Pp +Unfortunately, many of the popular Git hosting sites do not offer anonymous +access via SSH. +Such sites will require an account to be created, and a public SSH key to be +uploaded to this account, before repository access via ssh:// URLs will work. +.Pp +Use of HTTP URLs currently requires +.Xr git 1 : +.Pp +.Dl $ cd /var/git/ +.Dl $ git clone --bare https://github.com/openbsd/src.git +.Pp +Alternatively, for quick and dirty local testing of +.Nm +a new Git repository could be created and populated with files, +e.g. from a temporary CVS checkout located at +.Pa /tmp/src : +.Pp +.Dl $ gotadmin init /var/git/src.git +.Dl $ got import -r /var/git/src.git -I CVS -I obj /tmp/src +.Pp +Check out a work tree from the Git repository to /usr/src: +.Pp +.Dl $ got checkout /var/git/src.git /usr/src +.Pp +View local changes in a work tree directory: +.Pp +.Dl $ got diff | less +.Pp +In a work tree, display files in a potentially problematic state: +.Pp +.Dl $ got status -s 'C!~?' +.Pp +Interactively revert selected local changes in a work tree directory: +.Pp +.Dl $ got revert -p -R\ . +.Pp +In a work tree or a git repository directory, list all branch references: +.Pp +.Dl $ got branch -l +.Pp +As above, but list the most recently modified branches only: +.Pp +.Dl $ got branch -lt | head +.Pp +In a work tree or a git repository directory, create a new branch called +.Dq unified-buffer-cache +which is forked off the +.Dq master +branch: +.Pp +.Dl $ got branch -c master unified-buffer-cache +.Pp +Switch an existing work tree to the branch +.Dq unified-buffer-cache . +Local changes in the work tree will be preserved and merged if necessary: +.Pp +.Dl $ got update -b unified-buffer-cache +.Pp +Create a new commit from local changes in a work tree directory. +This new commit will become the head commit of the work tree's current branch: +.Pp +.Dl $ got commit +.Pp +In a work tree or a git repository directory, view changes committed in +the 3 most recent commits to the work tree's branch, or the branch resolved +via the repository's HEAD reference, respectively: +.Pp +.Dl $ got log -p -l 3 +.Pp +As above, but display changes in the order in which +.Xr patch 1 +could apply them in sequence: +.Pp +.Dl $ got log -p -l 3 -R +.Pp +In a work tree or a git repository directory, log the history of a subdirectory: +.Pp +.Dl $ got log sys/uvm +.Pp +While operating inside a work tree, paths are specified relative to the current +working directory, so this command will log the subdirectory +.Pa sys/uvm : +.Pp +.Dl $ cd sys/uvm && got log\ . +.Pp +And this command has the same effect: +.Pp +.Dl $ cd sys/dev/usb && got log ../../uvm +.Pp +And this command displays work tree meta-data about all tracked files: +.Pp +.Dl $ cd /usr/src +.Dl $ got info\ . | less +.Pp +Add new files and remove obsolete files in a work tree directory: +.Pp +.Dl $ got add sys/uvm/uvm_ubc.c +.Dl $ got remove sys/uvm/uvm_vnode.c +.Pp +Create a new commit from local changes in a work tree directory +with a pre-defined log message. +.Pp +.Dl $ got commit -m 'unify the buffer cache' +.Pp +Alternatively, create a new commit from local changes in a work tree +directory with a log message that has been prepared in the file +.Pa /tmp/msg : +.Pp +.Dl $ got commit -F /tmp/msg +.Pp +Update any work tree checked out from the +.Dq unified-buffer-cache +branch to the latest commit on this branch: +.Pp +.Dl $ got update +.Pp +Roll file content on the unified-buffer-cache branch back by one commit, +and then fetch the rolled-back change into the work tree as a local change +to be amended and perhaps committed again: +.Pp +.Dl $ got backout unified-buffer-cache +.Dl $ got commit -m 'roll back previous' +.Dl $ # now back out the previous backout :-) +.Dl $ got backout unified-buffer-cache +.Pp +Fetch new changes on the remote repository's +.Dq master +branch, making them visible on the local repository's +.Dq origin/master +branch: +.Pp +.Dl $ cd /usr/src +.Dl $ got fetch +.Pp +In a repository created with a HTTP URL and +.Cm git clone --bare +the +.Xr git-fetch 1 +command must be used instead: +.Pp +.Dl $ cd /var/git/src.git +.Dl $ git fetch origin master:refs/remotes/origin/master +.Pp +Rebase the local +.Dq master +branch to merge the new changes that are now visible on the +.Dq origin/master +branch: +.Pp +.Dl $ cd /usr/src +.Dl $ got update -b origin/master +.Dl $ got rebase master +.Pp +Rebase the +.Dq unified-buffer-cache +branch on top of the new head commit of the +.Dq master +branch. +.Pp +.Dl $ got update -b master +.Dl $ got rebase unified-buffer-cache +.Pp +Create a patch from all changes on the unified-buffer-cache branch. +The patch can be mailed out for review and applied to +.Ox Ns 's +CVS tree: +.Pp +.Dl $ got diff master unified-buffer-cache > /tmp/ubc.diff +.Pp +Edit the entire commit history of the +.Dq unified-buffer-cache +branch: +.Pp +.Dl $ got update -b unified-buffer-cache +.Dl $ got update -c master +.Dl $ got histedit +.Pp +Before working against existing branches in a repository cloned with +.Cm git clone --bare +instead of +.Cm got clone , +a Git +.Dq refspec +must be configured to map all references in the remote repository +into the +.Dq refs/remotes +namespace of the local repository. +This can be achieved by setting Git's +.Pa remote.origin.fetch +configuration variable to the value +.Dq +refs/heads/*:refs/remotes/origin/* +with the +.Cm git config +command: +.Pp +.Dl $ cd /var/git/repo +.Dl $ git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*' +.Pp +Additionally, the +.Dq mirror +option must be disabled: +.Pp +.Dl $ cd /var/git/repo +.Dl $ git config remote.origin.mirror false +.Pp +Alternatively, the following +.Xr git-fetch 1 +configuration item can be added manually to the Git repository's +.Pa config +file: +.Pp +.Dl [remote \&"origin\&"] +.Dl url = ... +.Dl fetch = +refs/heads/*:refs/remotes/origin/* +.Dl mirror = false +.Pp +This configuration leaves the local repository's +.Dq refs/heads +namespace free for use by local branches checked out with +.Cm got checkout +and, if needed, created with +.Cm got branch . +Branches in the +.Dq refs/remotes/origin +namespace can now be updated with incoming changes from the remote +repository with +.Cm got fetch +or +.Xr git-fetch 1 +without extra command line arguments. +Newly fetched changes can be examined with +.Cm got log . +.Pp +Display changes on the remote repository's version of the +.Dq master +branch, as of the last time +.Cm got fetch +was run: +.Pp +.Dl $ got log -c origin/master | less +.Pp +As shown here, most commands accept abbreviated reference names such as +.Dq origin/master +instead of +.Dq refs/remotes/origin/master . +The latter is only needed in case of ambiguity. +.Pp +.Cm got rebase +can be used to merge changes which are visible on the +.Dq origin/master +branch into the +.Dq master +branch. +This will also merge local changes, if any, with the incoming changes: +.Pp +.Dl $ got update -b origin/master +.Dl $ got rebase master +.Pp +In order to make changes committed to the +.Dq unified-buffer-cache +visible on the +.Dq master +branch, the +.Dq unified-buffer-cache +branch can be rebased onto the +.Dq master +branch: +.Pp +.Dl $ got update -b master +.Dl $ got rebase unified-buffer-cache +.Pp +Changes on the +.Dq unified-buffer-cache +branch can now be made visible on the +.Dq master +branch with +.Cm got integrate . +Because the rebase operation switched the work tree to the +.Dq unified-buffer-cache +branch, the work tree must be switched back to the +.Dq master +branch first: +.Pp +.Dl $ got update -b master +.Dl $ got integrate unified-buffer-cache +.Pp +On the +.Dq master +branch, log messages for local changes can now be amended with +.Dq OK +by other developers and any other important new information: +.Pp +.Dl $ got update -c origin/master +.Dl $ got histedit -m +.Pp +If the remote repository offers write access, local changes on the +.Dq master +branch can be sent to the remote repository with +.Cm got send . +Usually, +.Cm got send +can be run without further arguments. +The arguments shown here match defaults, provided the work tree's +current branch is the +.Dq master +branch: +.Pp +.Dl $ got send -b master origin +.Pp +If the remote repository requires the HTTPS protocol, the +.Xr git-push 1 +command must be used instead: +.Pp +.Dl $ cd /var/git/src.git +.Dl $ git push origin master +.Pp +When making contributions to projects which use the +.Dq pull request +workflow, SSH protocol repository access needs to be set up first. +Once an account has been created on a Git hosting site it should +be possible to upload a public SSH key for repository access +authentication. +.Pp +The +.Dq pull request +workflow will usually involve two remote repositories. +In the real-life example below, the +.Dq origin +repository was forked from the +.Dq upstream +repository by using the Git hosting site's web interface. +The +.Xr got.conf 5 +file in the local repository describes both remote repositories: +.Bd -literal -offset indent +# Jelmers's repository, which accepts pull requests +remote "upstream" { + server git@github.com + protocol ssh + repository "/jelmer/dulwich" + branch { "master" } +} + +# Stefan's fork, used as the default remote repository +remote "origin" { + server git@github.com + protocol ssh + repository "/stspdotname/dulwich" + branch { "master" } +} +.Ed +.Pp +With this configuration, Stefan can create commits on +.Dq refs/heads/master +and send them to the +.Dq origin +repository by running: +.Pp +.Dl $ got send -b master origin +.Pp +The changes can now be proposed to Jelmer by opening a pull request +via the Git hosting site's web interface. +If Jelmer requests further changes to be made, additional commits +can be created on the +.Dq master +branch and be added to the pull request by running +.Cd got send +again. +.Pp +If Jelmer prefers additional commits to be +.Dq squashed +then the following commands can be used to achieve this: +.Pp +.Dl $ got update -b master +.Dl $ got update -c origin/master +.Dl $ got histedit -f +.Dl $ got send -f -b master origin +.Pp +In addition to reviewing the pull request in the web user interface, +Jelmer can fetch the pull request's branch into his local repository +and create a local branch which contains the proposed changes: +.Pp +.Dl $ got fetch -R refs/pull/1046/head origin +.Dl $ got branch -c refs/remotes/origin/pull/1046/head pr1046 +.Pp +Once Jelmer has accepted the pull request, Stefan can fetch the +merged changes, and possibly several other new changes, by running: +.Pp +.Dl $ got fetch upstream +.Pp +The merged changes will now be visible under the reference +.Dq refs/remotes/upstream/master . +The local +.Dq master +branch can now be rebased on top of the latest changes +from upstream: +.Pp +.Dl $ got update -b upstream/master +.Dl $ got rebase master +.Pp +As an alternative to +.Cm got rebase , +branches can be merged with +.Cm got merge : +.Pp +.Dl $ got update -b master +.Dl $ got merge upstream/master +.Pp +The question of whether to rebase or merge branches is philosophical. +When in doubt, refer to the software project's policies set by project +maintainers. +.Pp +As a final step, the forked repository's copy of the master branch needs +to be kept in sync by sending the new changes there: +.Pp +.Dl $ got send -f -b master origin +.Pp +If multiple pull requests need to be managed in parallel, a separate branch +must be created for each pull request with +.Cm got branch . +Each such branch can then be used as above, in place of +.Dq refs/heads/master . +Changes for any accepted pull requests will still appear under +.Dq refs/remotes/upstream/master, +regardless of which branch was used in the forked repository to +create a pull request. +.Sh SEE ALSO +.Xr gotadmin 1 , +.Xr tog 1 , +.Xr git-repository 5 , +.Xr got-worktree 5 , +.Xr got.conf 5 , +.Xr gotwebd 8 +.Sh AUTHORS +.An Anthony J. Bentley Aq Mt bentley@openbsd.org +.An Christian Weisgerber Aq Mt naddy@openbsd.org +.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org +.An Josh Rickmar Aq Mt jrick@zettaport.com +.An Joshua Stein Aq Mt jcs@openbsd.org +.An Klemens Nanni Aq Mt kn@openbsd.org +.An Martin Pieuchot Aq Mt mpi@openbsd.org +.An Neels Hofmeyr Aq Mt neels@hofmeyr.de +.An Omar Polo Aq Mt op@openbsd.org +.An Ori Bernstein Aq Mt ori@openbsd.org +.An Sebastien Marie Aq Mt semarie@openbsd.org +.An Stefan Sperling Aq Mt stsp@openbsd.org +.An Steven McDonald Aq Mt steven@steven-mcdonald.id.au +.An Theo Buehler Aq Mt tb@openbsd.org +.An Thomas Adam Aq Mt thomas@xteddy.org +.An Tracey Emery Aq Mt tracey@traceyemery.net +.An Yang Zhong Aq Mt yzhong@freebsdfoundation.org +.Pp +Parts of +.Nm , +.Xr tog 1 , +and +.Xr gotwebd 8 +were derived from code under copyright by: +.Pp +.An Caldera International +.An Daniel Hartmeier +.An Esben Norby +.An Henning Brauer +.An HÃ¥kan Olsson +.An Ingo Schwarze +.An Jean-Francois Brousseau +.An Joris Vink +.An Jyri J. Virkki +.An Larry Wall +.An Markus Friedl +.An Niall O'Higgins +.An Niklas Hallqvist +.An Ray Lai +.An Ryan McBride +.An Theo de Raadt +.An Todd C. Miller +.An Xavier Santolaria +.Pp +.Nm +contains code contributed to the public domain by +.An Austin Appleby . +.Sh CAVEATS +.Nm +is a work-in-progress and some features remain to be implemented. +.Pp +At present, the user has to fall back on +.Xr git 1 +to perform some tasks. +In particular: +.Bl -bullet +.It +Reading from remote repositories over HTTP or HTTPS protocols requires +.Xr git-clone 1 +and +.Xr git-fetch 1 . +.It +Writing to remote repositories over HTTP or HTTPS protocols requires +.Xr git-push 1 . +.It +The creation of merge commits with more than two parent commits requires +.Xr git-merge 1 . +.It +In situations where files or directories were moved around +.Cm got +will not automatically merge changes to new locations and +.Xr git 1 +will usually produce better results. +.El blob - /dev/null blob + 33fa9d308c2a4c7ed10e872de41279f2c5ba44bc (mode 644) --- /dev/null +++ cvg/got.c @@ -0,0 +1,14305 @@ +/* + * Copyright (c) 2017 Martin Pieuchot + * Copyright (c) 2018, 2019, 2020 Stefan Sperling + * Copyright (c) 2020 Ori Bernstein + * + * 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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "got_version.h" +#include "got_error.h" +#include "got_object.h" +#include "got_reference.h" +#include "got_repository.h" +#include "got_path.h" +#include "got_cancel.h" +#include "got_worktree.h" +#include "got_diff.h" +#include "got_commit_graph.h" +#include "got_fetch.h" +#include "got_send.h" +#include "got_blame.h" +#include "got_privsep.h" +#include "got_opentemp.h" +#include "got_gotconfig.h" +#include "got_dial.h" +#include "got_patch.h" +#include "got_sigs.h" +#include "got_date.h" + +#ifndef nitems +#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) +#endif + +static volatile sig_atomic_t sigint_received; +static volatile sig_atomic_t sigpipe_received; + +static void +catch_sigint(int signo) +{ + sigint_received = 1; +} + +static void +catch_sigpipe(int signo) +{ + sigpipe_received = 1; +} + + +struct got_cmd { + const char *cmd_name; + const struct got_error *(*cmd_main)(int, char *[]); + void (*cmd_usage)(void); + const char *cmd_alias; +}; + +__dead static void usage(int, int); +__dead static void usage_import(void); +__dead static void usage_clone(void); +__dead static void usage_fetch(void); +__dead static void usage_checkout(void); +__dead static void usage_update(void); +__dead static void usage_log(void); +__dead static void usage_diff(void); +__dead static void usage_blame(void); +__dead static void usage_tree(void); +__dead static void usage_status(void); +__dead static void usage_ref(void); +__dead static void usage_branch(void); +__dead static void usage_tag(void); +__dead static void usage_add(void); +__dead static void usage_remove(void); +__dead static void usage_patch(void); +__dead static void usage_revert(void); +__dead static void usage_commit(void); +__dead static void usage_send(void); +__dead static void usage_cherrypick(void); +__dead static void usage_backout(void); +__dead static void usage_rebase(void); +__dead static void usage_histedit(void); +__dead static void usage_integrate(void); +__dead static void usage_merge(void); +__dead static void usage_stage(void); +__dead static void usage_unstage(void); +__dead static void usage_cat(void); +__dead static void usage_info(void); + +static const struct got_error* cmd_import(int, char *[]); +static const struct got_error* cmd_clone(int, char *[]); +static const struct got_error* cmd_fetch(int, char *[]); +static const struct got_error* cmd_checkout(int, char *[]); +static const struct got_error* cmd_update(int, char *[]); +static const struct got_error* cmd_log(int, char *[]); +static const struct got_error* cmd_diff(int, char *[]); +static const struct got_error* cmd_blame(int, char *[]); +static const struct got_error* cmd_tree(int, char *[]); +static const struct got_error* cmd_status(int, char *[]); +static const struct got_error* cmd_ref(int, char *[]); +static const struct got_error* cmd_branch(int, char *[]); +static const struct got_error* cmd_tag(int, char *[]); +static const struct got_error* cmd_add(int, char *[]); +static const struct got_error* cmd_remove(int, char *[]); +static const struct got_error* cmd_patch(int, char *[]); +static const struct got_error* cmd_revert(int, char *[]); +static const struct got_error* cmd_commit(int, char *[]); +static const struct got_error* cmd_send(int, char *[]); +static const struct got_error* cmd_cherrypick(int, char *[]); +static const struct got_error* cmd_backout(int, char *[]); +static const struct got_error* cmd_rebase(int, char *[]); +static const struct got_error* cmd_histedit(int, char *[]); +static const struct got_error* cmd_integrate(int, char *[]); +static const struct got_error* cmd_merge(int, char *[]); +static const struct got_error* cmd_stage(int, char *[]); +static const struct got_error* cmd_unstage(int, char *[]); +static const struct got_error* cmd_cat(int, char *[]); +static const struct got_error* cmd_info(int, char *[]); + +static const struct got_cmd got_commands[] = { + { "import", cmd_import, usage_import, "im" }, + { "clone", cmd_clone, usage_clone, "cl" }, + { "fetch", cmd_fetch, usage_fetch, "fe" }, + { "checkout", cmd_checkout, usage_checkout, "co" }, + { "update", cmd_update, usage_update, "up" }, + { "log", cmd_log, usage_log, "" }, + { "diff", cmd_diff, usage_diff, "di" }, + { "blame", cmd_blame, usage_blame, "bl" }, + { "tree", cmd_tree, usage_tree, "tr" }, + { "status", cmd_status, usage_status, "st" }, + { "ref", cmd_ref, usage_ref, "" }, + { "branch", cmd_branch, usage_branch, "br" }, + { "tag", cmd_tag, usage_tag, "" }, + { "add", cmd_add, usage_add, "" }, + { "remove", cmd_remove, usage_remove, "rm" }, + { "patch", cmd_patch, usage_patch, "pa" }, + { "revert", cmd_revert, usage_revert, "rv" }, + { "commit", cmd_commit, usage_commit, "ci" }, + { "send", cmd_send, usage_send, "se" }, + { "cherrypick", cmd_cherrypick, usage_cherrypick, "cy" }, + { "backout", cmd_backout, usage_backout, "bo" }, + { "rebase", cmd_rebase, usage_rebase, "rb" }, + { "histedit", cmd_histedit, usage_histedit, "he" }, + { "integrate", cmd_integrate, usage_integrate,"ig" }, + { "merge", cmd_merge, usage_merge, "mg" }, + { "stage", cmd_stage, usage_stage, "sg" }, + { "unstage", cmd_unstage, usage_unstage, "ug" }, + { "cat", cmd_cat, usage_cat, "" }, + { "info", cmd_info, usage_info, "" }, +}; + +static void +list_commands(FILE *fp) +{ + size_t i; + + fprintf(fp, "commands:"); + for (i = 0; i < nitems(got_commands); i++) { + const struct got_cmd *cmd = &got_commands[i]; + fprintf(fp, " %s", cmd->cmd_name); + } + fputc('\n', fp); +} + +__dead static void +option_conflict(char a, char b) +{ + errx(1, "-%c and -%c options are mutually exclusive", a, b); +} + +int +main(int argc, char *argv[]) +{ + const struct got_cmd *cmd; + size_t i; + int ch; + int hflag = 0, Vflag = 0; + static const struct option longopts[] = { + { "version", no_argument, NULL, 'V' }, + { NULL, 0, NULL, 0 } + }; + + setlocale(LC_CTYPE, ""); + + while ((ch = getopt_long(argc, argv, "+hV", longopts, NULL)) != -1) { + switch (ch) { + case 'h': + hflag = 1; + break; + case 'V': + Vflag = 1; + break; + default: + usage(hflag, 1); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + optind = 1; + optreset = 1; + + if (Vflag) { + got_version_print_str(); + return 0; + } + + if (argc <= 0) + usage(hflag, hflag ? 0 : 1); + + signal(SIGINT, catch_sigint); + signal(SIGPIPE, catch_sigpipe); + + for (i = 0; i < nitems(got_commands); i++) { + const struct got_error *error; + + cmd = &got_commands[i]; + + if (strcmp(cmd->cmd_name, argv[0]) != 0 && + strcmp(cmd->cmd_alias, argv[0]) != 0) + continue; + + if (hflag) + cmd->cmd_usage(); + + error = cmd->cmd_main(argc, argv); + if (error && error->code != GOT_ERR_CANCELLED && + error->code != GOT_ERR_PRIVSEP_EXIT && + !(sigpipe_received && + error->code == GOT_ERR_ERRNO && errno == EPIPE) && + !(sigint_received && + error->code == GOT_ERR_ERRNO && errno == EINTR)) { + fflush(stdout); + fprintf(stderr, "%s: %s\n", getprogname(), error->msg); + return 1; + } + + return 0; + } + + fprintf(stderr, "%s: unknown command '%s'\n", getprogname(), argv[0]); + list_commands(stderr); + return 1; +} + +__dead static void +usage(int hflag, int status) +{ + FILE *fp = (status == 0) ? stdout : stderr; + + fprintf(fp, "usage: %s [-hV] command [arg ...]\n", + getprogname()); + if (hflag) + list_commands(fp); + exit(status); +} + +static const struct got_error * +get_editor(char **abspath) +{ + const struct got_error *err = NULL; + const char *editor; + + *abspath = NULL; + + editor = getenv("VISUAL"); + if (editor == NULL) + editor = getenv("EDITOR"); + + if (editor) { + err = got_path_find_prog(abspath, editor); + if (err) + return err; + } + + if (*abspath == NULL) { + *abspath = strdup("/usr/bin/vi"); + if (*abspath == NULL) + return got_error_from_errno("strdup"); + } + + return NULL; +} + +static const struct got_error * +apply_unveil(const char *repo_path, int repo_read_only, + const char *worktree_path) +{ + const struct got_error *err; + +#ifdef PROFILE + if (unveil("gmon.out", "rwc") != 0) + return got_error_from_errno2("unveil", "gmon.out"); +#endif + if (repo_path && unveil(repo_path, repo_read_only ? "r" : "rwc") != 0) + return got_error_from_errno2("unveil", repo_path); + + if (worktree_path && unveil(worktree_path, "rwc") != 0) + return got_error_from_errno2("unveil", worktree_path); + + if (unveil(GOT_TMPDIR_STR, "rwc") != 0) + return got_error_from_errno2("unveil", GOT_TMPDIR_STR); + + err = got_privsep_unveil_exec_helpers(); + if (err != NULL) + return err; + + if (unveil(NULL, NULL) != 0) + return got_error_from_errno("unveil"); + + return NULL; +} + +__dead static void +usage_import(void) +{ + fprintf(stderr, "usage: %s import [-b branch] [-I pattern] [-m message] " + "[-r repository-path] directory\n", getprogname()); + exit(1); +} + +static int +spawn_editor(const char *editor, const char *file) +{ + pid_t pid; + sig_t sighup, sigint, sigquit; + int st = -1; + + sighup = signal(SIGHUP, SIG_IGN); + sigint = signal(SIGINT, SIG_IGN); + sigquit = signal(SIGQUIT, SIG_IGN); + + switch (pid = fork()) { + case -1: + goto doneediting; + case 0: + execl(editor, editor, file, (char *)NULL); + _exit(127); + } + + while (waitpid(pid, &st, 0) == -1) + if (errno != EINTR) + break; + +doneediting: + (void)signal(SIGHUP, sighup); + (void)signal(SIGINT, sigint); + (void)signal(SIGQUIT, sigquit); + + if (!WIFEXITED(st)) { + errno = EINTR; + return -1; + } + + return WEXITSTATUS(st); +} + +static const struct got_error * +read_logmsg(char **logmsg, size_t *len, FILE *fp, size_t filesize) +{ + const struct got_error *err = NULL; + char *line = NULL; + size_t linesize = 0; + + *logmsg = NULL; + *len = 0; + + if (fseeko(fp, 0L, SEEK_SET) == -1) + return got_error_from_errno("fseeko"); + + *logmsg = malloc(filesize + 1); + if (*logmsg == NULL) + return got_error_from_errno("malloc"); + (*logmsg)[0] = '\0'; + + while (getline(&line, &linesize, fp) != -1) { + if (line[0] == '#' || (*len == 0 && line[0] == '\n')) + continue; /* remove comments and leading empty lines */ + *len = strlcat(*logmsg, line, filesize + 1); + if (*len >= filesize + 1) { + err = got_error(GOT_ERR_NO_SPACE); + goto done; + } + } + if (ferror(fp)) { + err = got_ferror(fp, GOT_ERR_IO); + goto done; + } + + while (*len > 0 && (*logmsg)[*len - 1] == '\n') { + (*logmsg)[*len - 1] = '\0'; + (*len)--; + } +done: + free(line); + if (err) { + free(*logmsg); + *logmsg = NULL; + *len = 0; + } + return err; +} + +static const struct got_error * +edit_logmsg(char **logmsg, const char *editor, const char *logmsg_path, + const char *initial_content, size_t initial_content_len, + int require_modification) +{ + const struct got_error *err = NULL; + struct stat st, st2; + FILE *fp = NULL; + size_t logmsg_len; + + *logmsg = NULL; + + if (stat(logmsg_path, &st) == -1) + return got_error_from_errno2("stat", logmsg_path); + + if (spawn_editor(editor, logmsg_path) == -1) + return got_error_from_errno("failed spawning editor"); + + if (require_modification) { + struct timespec timeout; + + timeout.tv_sec = 0; + timeout.tv_nsec = 1; + nanosleep(&timeout, NULL); + } + + if (stat(logmsg_path, &st2) == -1) + return got_error_from_errno("stat"); + + if (require_modification && st.st_size == st2.st_size && + timespeccmp(&st.st_mtim, &st2.st_mtim, ==)) + return got_error_msg(GOT_ERR_COMMIT_MSG_EMPTY, + "no changes made to commit message, aborting"); + + fp = fopen(logmsg_path, "re"); + if (fp == NULL) { + err = got_error_from_errno("fopen"); + goto done; + } + + /* strip comments and leading/trailing newlines */ + err = read_logmsg(logmsg, &logmsg_len, fp, st2.st_size); + if (err) + goto done; + if (logmsg_len == 0) { + err = got_error_msg(GOT_ERR_COMMIT_MSG_EMPTY, + "commit message cannot be empty, aborting"); + goto done; + } +done: + if (fp && fclose(fp) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + if (err) { + free(*logmsg); + *logmsg = NULL; + } + return err; +} + +static const struct got_error * +collect_import_msg(char **logmsg, char **logmsg_path, const char *editor, + const char *path_dir, const char *branch_name) +{ + char *initial_content = NULL; + const struct got_error *err = NULL; + int initial_content_len; + int fd = -1; + + initial_content_len = asprintf(&initial_content, + "\n# %s to be imported to branch %s\n", path_dir, + branch_name); + if (initial_content_len == -1) + return got_error_from_errno("asprintf"); + + err = got_opentemp_named_fd(logmsg_path, &fd, + GOT_TMPDIR_STR "/got-importmsg", ""); + if (err) + goto done; + + if (write(fd, initial_content, initial_content_len) == -1) { + err = got_error_from_errno2("write", *logmsg_path); + goto done; + } + if (close(fd) == -1) { + err = got_error_from_errno2("close", *logmsg_path); + goto done; + } + fd = -1; + + err = edit_logmsg(logmsg, editor, *logmsg_path, initial_content, + initial_content_len, 1); +done: + if (fd != -1 && close(fd) == -1 && err == NULL) + err = got_error_from_errno2("close", *logmsg_path); + free(initial_content); + if (err) { + free(*logmsg_path); + *logmsg_path = NULL; + } + return err; +} + +static const struct got_error * +import_progress(void *arg, const char *path) +{ + printf("A %s\n", path); + return NULL; +} + +static const struct got_error * +valid_author(const char *author) +{ + const char *email = author; + + /* + * Git' expects the author (or committer) to be in the form + * "name ", which are mostly free form (see the + * "committer" description in git-fast-import(1)). We're only + * doing this to avoid git's object parser breaking on commits + * we create. + */ + + while (*author && *author != '\n' && *author != '<' && *author != '>') + author++; + if (author != email && *author == '<' && *(author - 1) != ' ') + return got_error_fmt(GOT_ERR_COMMIT_BAD_AUTHOR, "%s: space " + "between author name and email required", email); + if (*author++ != '<') + return got_error_fmt(GOT_ERR_COMMIT_NO_EMAIL, "%s", email); + while (*author && *author != '\n' && *author != '<' && *author != '>') + author++; + if (strcmp(author, ">") != 0) + return got_error_fmt(GOT_ERR_COMMIT_NO_EMAIL, "%s", email); + return NULL; +} + +static const struct got_error * +get_author(char **author, struct got_repository *repo, + struct got_worktree *worktree) +{ + const struct got_error *err = NULL; + const char *got_author = NULL, *name, *email; + const struct got_gotconfig *worktree_conf = NULL, *repo_conf = NULL; + + *author = NULL; + + if (worktree) + worktree_conf = got_worktree_get_gotconfig(worktree); + repo_conf = got_repo_get_gotconfig(repo); + + /* + * Priority of potential author information sources, from most + * significant to least significant: + * 1) work tree's .got/got.conf file + * 2) repository's got.conf file + * 3) repository's git config file + * 4) environment variables + * 5) global git config files (in user's home directory or /etc) + */ + + if (worktree_conf) + got_author = got_gotconfig_get_author(worktree_conf); + if (got_author == NULL) + got_author = got_gotconfig_get_author(repo_conf); + if (got_author == NULL) { + name = got_repo_get_gitconfig_author_name(repo); + email = got_repo_get_gitconfig_author_email(repo); + if (name && email) { + if (asprintf(author, "%s <%s>", name, email) == -1) + return got_error_from_errno("asprintf"); + return NULL; + } + + got_author = getenv("GOT_AUTHOR"); + if (got_author == NULL) { + name = got_repo_get_global_gitconfig_author_name(repo); + email = got_repo_get_global_gitconfig_author_email( + repo); + if (name && email) { + if (asprintf(author, "%s <%s>", name, email) + == -1) + return got_error_from_errno("asprintf"); + return NULL; + } + /* TODO: Look up user in password database? */ + return got_error(GOT_ERR_COMMIT_NO_AUTHOR); + } + } + + *author = strdup(got_author); + if (*author == NULL) + return got_error_from_errno("strdup"); + + err = valid_author(*author); + if (err) { + free(*author); + *author = NULL; + } + return err; +} + +static const struct got_error * +get_allowed_signers(char **allowed_signers, struct got_repository *repo, + struct got_worktree *worktree) +{ + const char *got_allowed_signers = NULL; + const struct got_gotconfig *worktree_conf = NULL, *repo_conf = NULL; + + *allowed_signers = NULL; + + if (worktree) + worktree_conf = got_worktree_get_gotconfig(worktree); + repo_conf = got_repo_get_gotconfig(repo); + + /* + * Priority of potential author information sources, from most + * significant to least significant: + * 1) work tree's .got/got.conf file + * 2) repository's got.conf file + */ + + if (worktree_conf) + got_allowed_signers = got_gotconfig_get_allowed_signers_file( + worktree_conf); + if (got_allowed_signers == NULL) + got_allowed_signers = got_gotconfig_get_allowed_signers_file( + repo_conf); + + if (got_allowed_signers) { + *allowed_signers = strdup(got_allowed_signers); + if (*allowed_signers == NULL) + return got_error_from_errno("strdup"); + } + return NULL; +} + +static const struct got_error * +get_revoked_signers(char **revoked_signers, struct got_repository *repo, + struct got_worktree *worktree) +{ + const char *got_revoked_signers = NULL; + const struct got_gotconfig *worktree_conf = NULL, *repo_conf = NULL; + + *revoked_signers = NULL; + + if (worktree) + worktree_conf = got_worktree_get_gotconfig(worktree); + repo_conf = got_repo_get_gotconfig(repo); + + /* + * Priority of potential author information sources, from most + * significant to least significant: + * 1) work tree's .got/got.conf file + * 2) repository's got.conf file + */ + + if (worktree_conf) + got_revoked_signers = got_gotconfig_get_revoked_signers_file( + worktree_conf); + if (got_revoked_signers == NULL) + got_revoked_signers = got_gotconfig_get_revoked_signers_file( + repo_conf); + + if (got_revoked_signers) { + *revoked_signers = strdup(got_revoked_signers); + if (*revoked_signers == NULL) + return got_error_from_errno("strdup"); + } + return NULL; +} + +static const char * +get_signer_id(struct got_repository *repo, struct got_worktree *worktree) +{ + const char *got_signer_id = NULL; + const struct got_gotconfig *worktree_conf = NULL, *repo_conf = NULL; + + if (worktree) + worktree_conf = got_worktree_get_gotconfig(worktree); + repo_conf = got_repo_get_gotconfig(repo); + + /* + * Priority of potential author information sources, from most + * significant to least significant: + * 1) work tree's .got/got.conf file + * 2) repository's got.conf file + */ + + if (worktree_conf) + got_signer_id = got_gotconfig_get_signer_id(worktree_conf); + if (got_signer_id == NULL) + got_signer_id = got_gotconfig_get_signer_id(repo_conf); + + return got_signer_id; +} + +static const struct got_error * +get_gitconfig_path(char **gitconfig_path) +{ + const char *homedir = getenv("HOME"); + + *gitconfig_path = NULL; + if (homedir) { + if (asprintf(gitconfig_path, "%s/.gitconfig", homedir) == -1) + return got_error_from_errno("asprintf"); + + } + return NULL; +} + +static const struct got_error * +cmd_import(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + char *path_dir = NULL, *repo_path = NULL, *logmsg = NULL; + char *gitconfig_path = NULL, *editor = NULL, *author = NULL; + const char *branch_name = NULL; + char *id_str = NULL, *logmsg_path = NULL; + char refname[PATH_MAX] = "refs/heads/"; + struct got_repository *repo = NULL; + struct got_reference *branch_ref = NULL, *head_ref = NULL; + struct got_object_id *new_commit_id = NULL; + int ch, n = 0; + struct got_pathlist_head ignores; + struct got_pathlist_entry *pe; + int preserve_logmsg = 0; + int *pack_fds = NULL; + + TAILQ_INIT(&ignores); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd " + "unveil", + NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "b:I:m:r:")) != -1) { + switch (ch) { + case 'b': + branch_name = optarg; + break; + case 'I': + if (optarg[0] == '\0') + break; + error = got_pathlist_insert(&pe, &ignores, optarg, + NULL); + if (error) + goto done; + break; + case 'm': + logmsg = strdup(optarg); + if (logmsg == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + break; + case 'r': + repo_path = realpath(optarg, NULL); + if (repo_path == NULL) { + error = got_error_from_errno2("realpath", + optarg); + goto done; + } + break; + default: + usage_import(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (argc != 1) + usage_import(); + + if (repo_path == NULL) { + repo_path = getcwd(NULL, 0); + if (repo_path == NULL) + return got_error_from_errno("getcwd"); + } + got_path_strip_trailing_slashes(repo_path); + error = get_gitconfig_path(&gitconfig_path); + if (error) + goto done; + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + error = got_repo_open(&repo, repo_path, gitconfig_path, pack_fds); + if (error) + goto done; + + error = get_author(&author, repo, NULL); + if (error) + return error; + + /* + * Don't let the user create a branch name with a leading '-'. + * While technically a valid reference name, this case is usually + * an unintended typo. + */ + if (branch_name && branch_name[0] == '-') + return got_error_path(branch_name, GOT_ERR_REF_NAME_MINUS); + + error = got_ref_open(&head_ref, repo, GOT_REF_HEAD, 0); + if (error && error->code != GOT_ERR_NOT_REF) + goto done; + + if (branch_name) + n = strlcat(refname, branch_name, sizeof(refname)); + else if (head_ref && got_ref_is_symbolic(head_ref)) + n = strlcpy(refname, got_ref_get_symref_target(head_ref), + sizeof(refname)); + else + n = strlcat(refname, "main", sizeof(refname)); + if (n >= sizeof(refname)) { + error = got_error(GOT_ERR_NO_SPACE); + goto done; + } + + error = got_ref_open(&branch_ref, repo, refname, 0); + if (error) { + if (error->code != GOT_ERR_NOT_REF) + goto done; + } else { + error = got_error_msg(GOT_ERR_BRANCH_EXISTS, + "import target branch already exists"); + goto done; + } + + path_dir = realpath(argv[0], NULL); + if (path_dir == NULL) { + error = got_error_from_errno2("realpath", argv[0]); + goto done; + } + got_path_strip_trailing_slashes(path_dir); + + /* + * unveil(2) traverses exec(2); if an editor is used we have + * to apply unveil after the log message has been written. + */ + if (logmsg == NULL || *logmsg == '\0') { + error = get_editor(&editor); + if (error) + goto done; + free(logmsg); + error = collect_import_msg(&logmsg, &logmsg_path, editor, + path_dir, refname); + if (error) { + if (error->code != GOT_ERR_COMMIT_MSG_EMPTY && + logmsg_path != NULL) + preserve_logmsg = 1; + goto done; + } + } + + if (unveil(path_dir, "r") != 0) { + error = got_error_from_errno2("unveil", path_dir); + if (logmsg_path) + preserve_logmsg = 1; + goto done; + } + + error = apply_unveil(got_repo_get_path(repo), 0, NULL); + if (error) { + if (logmsg_path) + preserve_logmsg = 1; + goto done; + } + + error = got_repo_import(&new_commit_id, path_dir, logmsg, + author, &ignores, repo, import_progress, NULL); + if (error) { + if (logmsg_path) + preserve_logmsg = 1; + goto done; + } + + error = got_ref_alloc(&branch_ref, refname, new_commit_id); + if (error) { + if (logmsg_path) + preserve_logmsg = 1; + goto done; + } + + error = got_ref_write(branch_ref, repo); + if (error) { + if (logmsg_path) + preserve_logmsg = 1; + goto done; + } + + error = got_object_id_str(&id_str, new_commit_id); + if (error) { + if (logmsg_path) + preserve_logmsg = 1; + goto done; + } + + error = got_ref_open(&head_ref, repo, GOT_REF_HEAD, 0); + if (error) { + if (error->code != GOT_ERR_NOT_REF) { + if (logmsg_path) + preserve_logmsg = 1; + goto done; + } + + error = got_ref_alloc_symref(&head_ref, GOT_REF_HEAD, + branch_ref); + if (error) { + if (logmsg_path) + preserve_logmsg = 1; + goto done; + } + + error = got_ref_write(head_ref, repo); + if (error) { + if (logmsg_path) + preserve_logmsg = 1; + goto done; + } + } + + printf("Created branch %s with commit %s\n", + got_ref_get_name(branch_ref), id_str); +done: + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (preserve_logmsg) { + fprintf(stderr, "%s: log message preserved in %s\n", + getprogname(), logmsg_path); + } else if (logmsg_path && unlink(logmsg_path) == -1 && error == NULL) + error = got_error_from_errno2("unlink", logmsg_path); + free(logmsg); + free(logmsg_path); + free(repo_path); + free(editor); + free(new_commit_id); + free(id_str); + free(author); + free(gitconfig_path); + if (branch_ref) + got_ref_close(branch_ref); + if (head_ref) + got_ref_close(head_ref); + return error; +} + +__dead static void +usage_clone(void) +{ + fprintf(stderr, "usage: %s clone [-almqv] [-b branch] [-R reference] " + "repository-URL [directory]\n", getprogname()); + exit(1); +} + +struct got_fetch_progress_arg { + char last_scaled_size[FMT_SCALED_STRSIZE]; + int last_p_indexed; + int last_p_resolved; + int verbosity; + + struct got_repository *repo; + + int create_configs; + int configs_created; + struct { + struct got_pathlist_head *symrefs; + struct got_pathlist_head *wanted_branches; + struct got_pathlist_head *wanted_refs; + const char *proto; + const char *host; + const char *port; + const char *remote_repo_path; + const char *git_url; + int fetch_all_branches; + int mirror_references; + } config_info; +}; + +/* XXX forward declaration */ +static const struct got_error * +create_config_files(const char *proto, const char *host, const char *port, + const char *remote_repo_path, const char *git_url, int fetch_all_branches, + int mirror_references, struct got_pathlist_head *symrefs, + struct got_pathlist_head *wanted_branches, + struct got_pathlist_head *wanted_refs, struct got_repository *repo); + +static const struct got_error * +fetch_progress(void *arg, const char *message, off_t packfile_size, + int nobj_total, int nobj_indexed, int nobj_loose, int nobj_resolved) +{ + const struct got_error *err = NULL; + struct got_fetch_progress_arg *a = arg; + char scaled_size[FMT_SCALED_STRSIZE]; + int p_indexed, p_resolved; + int print_size = 0, print_indexed = 0, print_resolved = 0; + + /* + * In order to allow a failed clone to be resumed with 'got fetch' + * we try to create configuration files as soon as possible. + * Once the server has sent information about its default branch + * we have all required information. + */ + if (a->create_configs && !a->configs_created && + !TAILQ_EMPTY(a->config_info.symrefs)) { + err = create_config_files(a->config_info.proto, + a->config_info.host, a->config_info.port, + a->config_info.remote_repo_path, + a->config_info.git_url, + a->config_info.fetch_all_branches, + a->config_info.mirror_references, + a->config_info.symrefs, + a->config_info.wanted_branches, + a->config_info.wanted_refs, a->repo); + if (err) + return err; + a->configs_created = 1; + } + + if (a->verbosity < 0) + return NULL; + + if (message && message[0] != '\0') { + printf("\rserver: %s", message); + fflush(stdout); + return NULL; + } + + if (packfile_size > 0 || nobj_indexed > 0) { + if (fmt_scaled(packfile_size, scaled_size) == 0 && + (a->last_scaled_size[0] == '\0' || + strcmp(scaled_size, a->last_scaled_size)) != 0) { + print_size = 1; + if (strlcpy(a->last_scaled_size, scaled_size, + FMT_SCALED_STRSIZE) >= FMT_SCALED_STRSIZE) + return got_error(GOT_ERR_NO_SPACE); + } + if (nobj_indexed > 0) { + p_indexed = (nobj_indexed * 100) / nobj_total; + if (p_indexed != a->last_p_indexed) { + a->last_p_indexed = p_indexed; + print_indexed = 1; + print_size = 1; + } + } + if (nobj_resolved > 0) { + p_resolved = (nobj_resolved * 100) / + (nobj_total - nobj_loose); + if (p_resolved != a->last_p_resolved) { + a->last_p_resolved = p_resolved; + print_resolved = 1; + print_indexed = 1; + print_size = 1; + } + } + + } + if (print_size || print_indexed || print_resolved) + printf("\r"); + if (print_size) + printf("%*s fetched", FMT_SCALED_STRSIZE - 2, scaled_size); + if (print_indexed) + printf("; indexing %d%%", p_indexed); + if (print_resolved) + printf("; resolving deltas %d%%", p_resolved); + if (print_size || print_indexed || print_resolved) + fflush(stdout); + + return NULL; +} + +static const struct got_error * +create_symref(const char *refname, struct got_reference *target_ref, + int verbosity, struct got_repository *repo) +{ + const struct got_error *err; + struct got_reference *head_symref; + + err = got_ref_alloc_symref(&head_symref, refname, target_ref); + if (err) + return err; + + err = got_ref_write(head_symref, repo); + if (err == NULL && verbosity > 0) { + printf("Created reference %s: %s\n", GOT_REF_HEAD, + got_ref_get_name(target_ref)); + } + got_ref_close(head_symref); + return err; +} + +static const struct got_error * +list_remote_refs(struct got_pathlist_head *symrefs, + struct got_pathlist_head *refs) +{ + const struct got_error *err; + struct got_pathlist_entry *pe; + + TAILQ_FOREACH(pe, symrefs, entry) { + const char *refname = pe->path; + const char *targetref = pe->data; + + printf("%s: %s\n", refname, targetref); + } + + TAILQ_FOREACH(pe, refs, entry) { + const char *refname = pe->path; + struct got_object_id *id = pe->data; + char *id_str; + + err = got_object_id_str(&id_str, id); + if (err) + return err; + printf("%s: %s\n", refname, id_str); + free(id_str); + } + + return NULL; +} + +static const struct got_error * +create_ref(const char *refname, struct got_object_id *id, + int verbosity, struct got_repository *repo) +{ + const struct got_error *err = NULL; + struct got_reference *ref; + char *id_str; + + err = got_object_id_str(&id_str, id); + if (err) + return err; + + err = got_ref_alloc(&ref, refname, id); + if (err) + goto done; + + err = got_ref_write(ref, repo); + got_ref_close(ref); + + if (err == NULL && verbosity >= 0) + printf("Created reference %s: %s\n", refname, id_str); +done: + free(id_str); + return err; +} + +static int +match_wanted_ref(const char *refname, const char *wanted_ref) +{ + if (strncmp(refname, "refs/", 5) != 0) + return 0; + refname += 5; + + /* + * Prevent fetching of references that won't make any + * sense outside of the remote repository's context. + */ + if (strncmp(refname, "got/", 4) == 0) + return 0; + if (strncmp(refname, "remotes/", 8) == 0) + return 0; + + if (strncmp(wanted_ref, "refs/", 5) == 0) + wanted_ref += 5; + + /* Allow prefix match. */ + if (got_path_is_child(refname, wanted_ref, strlen(wanted_ref))) + return 1; + + /* Allow exact match. */ + return (strcmp(refname, wanted_ref) == 0); +} + +static int +is_wanted_ref(struct got_pathlist_head *wanted_refs, const char *refname) +{ + struct got_pathlist_entry *pe; + + TAILQ_FOREACH(pe, wanted_refs, entry) { + if (match_wanted_ref(refname, pe->path)) + return 1; + } + + return 0; +} + +static const struct got_error * +create_wanted_ref(const char *refname, struct got_object_id *id, + const char *remote_repo_name, int verbosity, struct got_repository *repo) +{ + const struct got_error *err; + char *remote_refname; + + if (strncmp("refs/", refname, 5) == 0) + refname += 5; + + if (asprintf(&remote_refname, "refs/remotes/%s/%s", + remote_repo_name, refname) == -1) + return got_error_from_errno("asprintf"); + + err = create_ref(remote_refname, id, verbosity, repo); + free(remote_refname); + return err; +} + +static const struct got_error * +create_gotconfig(const char *proto, const char *host, const char *port, + const char *remote_repo_path, const char *default_branch, + int fetch_all_branches, struct got_pathlist_head *wanted_branches, + struct got_pathlist_head *wanted_refs, int mirror_references, + struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *gotconfig_path = NULL; + char *gotconfig = NULL; + FILE *gotconfig_file = NULL; + const char *branchname = NULL; + char *branches = NULL, *refs = NULL; + ssize_t n; + + if (!fetch_all_branches && !TAILQ_EMPTY(wanted_branches)) { + struct got_pathlist_entry *pe; + TAILQ_FOREACH(pe, wanted_branches, entry) { + char *s; + branchname = pe->path; + if (strncmp(branchname, "refs/heads/", 11) == 0) + branchname += 11; + if (asprintf(&s, "%s\"%s\" ", + branches ? branches : "", branchname) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + free(branches); + branches = s; + } + } else if (!fetch_all_branches && default_branch) { + branchname = default_branch; + if (strncmp(branchname, "refs/heads/", 11) == 0) + branchname += 11; + if (asprintf(&branches, "\"%s\" ", branchname) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + } + if (!TAILQ_EMPTY(wanted_refs)) { + struct got_pathlist_entry *pe; + TAILQ_FOREACH(pe, wanted_refs, entry) { + char *s; + const char *refname = pe->path; + if (strncmp(refname, "refs/", 5) == 0) + branchname += 5; + if (asprintf(&s, "%s\"%s\" ", + refs ? refs : "", refname) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + free(refs); + refs = s; + } + } + + /* Create got.conf(5). */ + gotconfig_path = got_repo_get_path_gotconfig(repo); + if (gotconfig_path == NULL) { + err = got_error_from_errno("got_repo_get_path_gotconfig"); + goto done; + } + gotconfig_file = fopen(gotconfig_path, "ae"); + if (gotconfig_file == NULL) { + err = got_error_from_errno2("fopen", gotconfig_path); + goto done; + } + if (asprintf(&gotconfig, + "remote \"%s\" {\n" + "\tserver %s\n" + "\tprotocol %s\n" + "%s%s%s" + "\trepository \"%s\"\n" + "%s%s%s" + "%s%s%s" + "%s" + "%s" + "}\n", + GOT_FETCH_DEFAULT_REMOTE_NAME, host, proto, + port ? "\tport " : "", port ? port : "", port ? "\n" : "", + remote_repo_path, branches ? "\tbranch { " : "", + branches ? branches : "", branches ? "}\n" : "", + refs ? "\treference { " : "", refs ? refs : "", refs ? "}\n" : "", + mirror_references ? "\tmirror_references yes\n" : "", + fetch_all_branches ? "\tfetch_all_branches yes\n" : "") == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + n = fwrite(gotconfig, 1, strlen(gotconfig), gotconfig_file); + if (n != strlen(gotconfig)) { + err = got_ferror(gotconfig_file, GOT_ERR_IO); + goto done; + } + +done: + if (gotconfig_file && fclose(gotconfig_file) == EOF && err == NULL) + err = got_error_from_errno2("fclose", gotconfig_path); + free(gotconfig_path); + free(branches); + return err; +} + +static const struct got_error * +create_gitconfig(const char *git_url, const char *default_branch, + int fetch_all_branches, struct got_pathlist_head *wanted_branches, + struct got_pathlist_head *wanted_refs, int mirror_references, + struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *gitconfig_path = NULL; + char *gitconfig = NULL; + FILE *gitconfig_file = NULL; + char *branches = NULL, *refs = NULL; + const char *branchname; + ssize_t n; + + /* Create a config file Git can understand. */ + gitconfig_path = got_repo_get_path_gitconfig(repo); + if (gitconfig_path == NULL) { + err = got_error_from_errno("got_repo_get_path_gitconfig"); + goto done; + } + gitconfig_file = fopen(gitconfig_path, "ae"); + if (gitconfig_file == NULL) { + err = got_error_from_errno2("fopen", gitconfig_path); + goto done; + } + if (fetch_all_branches) { + if (mirror_references) { + if (asprintf(&branches, + "\tfetch = refs/heads/*:refs/heads/*\n") == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + } else if (asprintf(&branches, + "\tfetch = refs/heads/*:refs/remotes/%s/*\n", + GOT_FETCH_DEFAULT_REMOTE_NAME) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + } else if (!TAILQ_EMPTY(wanted_branches)) { + struct got_pathlist_entry *pe; + TAILQ_FOREACH(pe, wanted_branches, entry) { + char *s; + branchname = pe->path; + if (strncmp(branchname, "refs/heads/", 11) == 0) + branchname += 11; + if (mirror_references) { + if (asprintf(&s, + "%s\tfetch = refs/heads/%s:refs/heads/%s\n", + branches ? branches : "", + branchname, branchname) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + } else if (asprintf(&s, + "%s\tfetch = refs/heads/%s:refs/remotes/%s/%s\n", + branches ? branches : "", + branchname, GOT_FETCH_DEFAULT_REMOTE_NAME, + branchname) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + free(branches); + branches = s; + } + } else { + /* + * If the server specified a default branch, use just that one. + * Otherwise fall back to fetching all branches on next fetch. + */ + if (default_branch) { + branchname = default_branch; + if (strncmp(branchname, "refs/heads/", 11) == 0) + branchname += 11; + } else + branchname = "*"; /* fall back to all branches */ + if (mirror_references) { + if (asprintf(&branches, + "\tfetch = refs/heads/%s:refs/heads/%s\n", + branchname, branchname) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + } else if (asprintf(&branches, + "\tfetch = refs/heads/%s:refs/remotes/%s/%s\n", + branchname, GOT_FETCH_DEFAULT_REMOTE_NAME, + branchname) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + } + if (!TAILQ_EMPTY(wanted_refs)) { + struct got_pathlist_entry *pe; + TAILQ_FOREACH(pe, wanted_refs, entry) { + char *s; + const char *refname = pe->path; + if (strncmp(refname, "refs/", 5) == 0) + refname += 5; + if (mirror_references) { + if (asprintf(&s, + "%s\tfetch = refs/%s:refs/%s\n", + refs ? refs : "", refname, refname) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + } else if (asprintf(&s, + "%s\tfetch = refs/%s:refs/remotes/%s/%s\n", + refs ? refs : "", + refname, GOT_FETCH_DEFAULT_REMOTE_NAME, + refname) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + free(refs); + refs = s; + } + } + + if (asprintf(&gitconfig, + "[remote \"%s\"]\n" + "\turl = %s\n" + "%s" + "%s" + "\tfetch = refs/tags/*:refs/tags/*\n", + GOT_FETCH_DEFAULT_REMOTE_NAME, git_url, branches ? branches : "", + refs ? refs : "") == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + n = fwrite(gitconfig, 1, strlen(gitconfig), gitconfig_file); + if (n != strlen(gitconfig)) { + err = got_ferror(gitconfig_file, GOT_ERR_IO); + goto done; + } +done: + if (gitconfig_file && fclose(gitconfig_file) == EOF && err == NULL) + err = got_error_from_errno2("fclose", gitconfig_path); + free(gitconfig_path); + free(branches); + return err; +} + +static const struct got_error * +create_config_files(const char *proto, const char *host, const char *port, + const char *remote_repo_path, const char *git_url, int fetch_all_branches, + int mirror_references, struct got_pathlist_head *symrefs, + struct got_pathlist_head *wanted_branches, + struct got_pathlist_head *wanted_refs, struct got_repository *repo) +{ + const struct got_error *err = NULL; + const char *default_branch = NULL; + struct got_pathlist_entry *pe; + + /* + * If we asked for a set of wanted branches then use the first + * one of those. + */ + if (!TAILQ_EMPTY(wanted_branches)) { + pe = TAILQ_FIRST(wanted_branches); + default_branch = pe->path; + } else { + /* First HEAD ref listed by server is the default branch. */ + TAILQ_FOREACH(pe, symrefs, entry) { + const char *refname = pe->path; + const char *target = pe->data; + + if (strcmp(refname, GOT_REF_HEAD) != 0) + continue; + + default_branch = target; + break; + } + } + + /* Create got.conf(5). */ + err = create_gotconfig(proto, host, port, remote_repo_path, + default_branch, fetch_all_branches, wanted_branches, + wanted_refs, mirror_references, repo); + if (err) + return err; + + /* Create a config file Git can understand. */ + return create_gitconfig(git_url, default_branch, fetch_all_branches, + wanted_branches, wanted_refs, mirror_references, repo); +} + +static const struct got_error * +cmd_clone(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + const char *uri, *dirname; + char *proto, *host, *port, *repo_name, *server_path; + char *default_destdir = NULL, *id_str = NULL; + const char *repo_path; + struct got_repository *repo = NULL; + struct got_pathlist_head refs, symrefs, wanted_branches, wanted_refs; + struct got_pathlist_entry *pe; + struct got_object_id *pack_hash = NULL; + int ch, fetchfd = -1, fetchstatus; + pid_t fetchpid = -1; + struct got_fetch_progress_arg fpa; + char *git_url = NULL; + int verbosity = 0, fetch_all_branches = 0, mirror_references = 0; + int bflag = 0, list_refs_only = 0; + int *pack_fds = NULL; + + TAILQ_INIT(&refs); + TAILQ_INIT(&symrefs); + TAILQ_INIT(&wanted_branches); + TAILQ_INIT(&wanted_refs); + + while ((ch = getopt(argc, argv, "ab:lmqR:v")) != -1) { + switch (ch) { + case 'a': + fetch_all_branches = 1; + break; + case 'b': + error = got_pathlist_append(&wanted_branches, + optarg, NULL); + if (error) + return error; + bflag = 1; + break; + case 'l': + list_refs_only = 1; + break; + case 'm': + mirror_references = 1; + break; + case 'q': + verbosity = -1; + break; + case 'R': + error = got_pathlist_append(&wanted_refs, + optarg, NULL); + if (error) + return error; + break; + case 'v': + if (verbosity < 0) + verbosity = 0; + else if (verbosity < 3) + verbosity++; + break; + default: + usage_clone(); + break; + } + } + argc -= optind; + argv += optind; + + if (fetch_all_branches && !TAILQ_EMPTY(&wanted_branches)) + option_conflict('a', 'b'); + if (list_refs_only) { + if (!TAILQ_EMPTY(&wanted_branches)) + option_conflict('l', 'b'); + if (fetch_all_branches) + option_conflict('l', 'a'); + if (mirror_references) + option_conflict('l', 'm'); + if (!TAILQ_EMPTY(&wanted_refs)) + option_conflict('l', 'R'); + } + + uri = argv[0]; + + if (argc == 1) + dirname = NULL; + else if (argc == 2) + dirname = argv[1]; + else + usage_clone(); + + error = got_dial_parse_uri(&proto, &host, &port, &server_path, + &repo_name, uri); + if (error) + goto done; + + if (asprintf(&git_url, "%s://%s%s%s%s%s", proto, + host, port ? ":" : "", port ? port : "", + server_path[0] != '/' ? "/" : "", server_path) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + + if (strcmp(proto, "git") == 0) { +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec " + "sendfd dns inet unveil", NULL) == -1) + err(1, "pledge"); +#endif + } else if (strcmp(proto, "git+ssh") == 0 || + strcmp(proto, "ssh") == 0) { +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec " + "sendfd unveil", NULL) == -1) + err(1, "pledge"); +#endif + } else if (strcmp(proto, "http") == 0 || + strcmp(proto, "git+http") == 0) { + error = got_error_path(proto, GOT_ERR_NOT_IMPL); + goto done; + } else { + error = got_error_path(proto, GOT_ERR_BAD_PROTO); + goto done; + } + if (dirname == NULL) { + if (asprintf(&default_destdir, "%s.git", repo_name) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + repo_path = default_destdir; + } else + repo_path = dirname; + + if (!list_refs_only) { + error = got_path_mkdir(repo_path); + if (error && + (!(error->code == GOT_ERR_ERRNO && errno == EISDIR) && + !(error->code == GOT_ERR_ERRNO && errno == EEXIST))) + goto done; + if (!got_path_dir_is_empty(repo_path)) { + error = got_error_path(repo_path, + GOT_ERR_DIR_NOT_EMPTY); + goto done; + } + } + + error = got_dial_apply_unveil(proto); + if (error) + goto done; + + error = apply_unveil(repo_path, 0, NULL); + if (error) + goto done; + + if (verbosity >= 0) + printf("Connecting to %s\n", git_url); + + error = got_fetch_connect(&fetchpid, &fetchfd, proto, host, port, + server_path, verbosity); + if (error) + goto done; + + if (!list_refs_only) { + error = got_repo_init(repo_path, NULL); + if (error) + goto done; + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + error = got_repo_open(&repo, repo_path, NULL, pack_fds); + if (error) + goto done; + } + + fpa.last_scaled_size[0] = '\0'; + fpa.last_p_indexed = -1; + fpa.last_p_resolved = -1; + fpa.verbosity = verbosity; + fpa.create_configs = 1; + fpa.configs_created = 0; + fpa.repo = repo; + fpa.config_info.symrefs = &symrefs; + fpa.config_info.wanted_branches = &wanted_branches; + fpa.config_info.wanted_refs = &wanted_refs; + fpa.config_info.proto = proto; + fpa.config_info.host = host; + fpa.config_info.port = port; + fpa.config_info.remote_repo_path = server_path; + fpa.config_info.git_url = git_url; + fpa.config_info.fetch_all_branches = fetch_all_branches; + fpa.config_info.mirror_references = mirror_references; + error = got_fetch_pack(&pack_hash, &refs, &symrefs, + GOT_FETCH_DEFAULT_REMOTE_NAME, mirror_references, + fetch_all_branches, &wanted_branches, &wanted_refs, + list_refs_only, verbosity, fetchfd, repo, NULL, NULL, bflag, + fetch_progress, &fpa); + if (error) + goto done; + + if (list_refs_only) { + error = list_remote_refs(&symrefs, &refs); + goto done; + } + + if (pack_hash == NULL) { + error = got_error_fmt(GOT_ERR_FETCH_FAILED, "%s", + "server sent an empty pack file"); + goto done; + } + error = got_object_id_str(&id_str, pack_hash); + if (error) + goto done; + if (verbosity >= 0) + printf("\nFetched %s.pack\n", id_str); + free(id_str); + + /* Set up references provided with the pack file. */ + TAILQ_FOREACH(pe, &refs, entry) { + const char *refname = pe->path; + struct got_object_id *id = pe->data; + char *remote_refname; + + if (is_wanted_ref(&wanted_refs, refname) && + !mirror_references) { + error = create_wanted_ref(refname, id, + GOT_FETCH_DEFAULT_REMOTE_NAME, + verbosity - 1, repo); + if (error) + goto done; + continue; + } + + error = create_ref(refname, id, verbosity - 1, repo); + if (error) + goto done; + + if (mirror_references) + continue; + + if (strncmp("refs/heads/", refname, 11) != 0) + continue; + + if (asprintf(&remote_refname, + "refs/remotes/%s/%s", GOT_FETCH_DEFAULT_REMOTE_NAME, + refname + 11) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + error = create_ref(remote_refname, id, verbosity - 1, repo); + free(remote_refname); + if (error) + goto done; + } + + /* Set the HEAD reference if the server provided one. */ + TAILQ_FOREACH(pe, &symrefs, entry) { + struct got_reference *target_ref; + const char *refname = pe->path; + const char *target = pe->data; + char *remote_refname = NULL, *remote_target = NULL; + + if (strcmp(refname, GOT_REF_HEAD) != 0) + continue; + + error = got_ref_open(&target_ref, repo, target, 0); + if (error) { + if (error->code == GOT_ERR_NOT_REF) { + error = NULL; + continue; + } + goto done; + } + + error = create_symref(refname, target_ref, verbosity, repo); + got_ref_close(target_ref); + if (error) + goto done; + + if (mirror_references) + continue; + + if (strncmp("refs/heads/", target, 11) != 0) + continue; + + if (asprintf(&remote_refname, + "refs/remotes/%s/%s", GOT_FETCH_DEFAULT_REMOTE_NAME, + refname) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + if (asprintf(&remote_target, + "refs/remotes/%s/%s", GOT_FETCH_DEFAULT_REMOTE_NAME, + target + 11) == -1) { + error = got_error_from_errno("asprintf"); + free(remote_refname); + goto done; + } + error = got_ref_open(&target_ref, repo, remote_target, 0); + if (error) { + free(remote_refname); + free(remote_target); + if (error->code == GOT_ERR_NOT_REF) { + error = NULL; + continue; + } + goto done; + } + error = create_symref(remote_refname, target_ref, + verbosity - 1, repo); + free(remote_refname); + free(remote_target); + got_ref_close(target_ref); + if (error) + goto done; + } + if (pe == NULL) { + /* + * We failed to set the HEAD reference. If we asked for + * a set of wanted branches use the first of one of those + * which could be fetched instead. + */ + TAILQ_FOREACH(pe, &wanted_branches, entry) { + const char *target = pe->path; + struct got_reference *target_ref; + + error = got_ref_open(&target_ref, repo, target, 0); + if (error) { + if (error->code == GOT_ERR_NOT_REF) { + error = NULL; + continue; + } + goto done; + } + + error = create_symref(GOT_REF_HEAD, target_ref, + verbosity, repo); + got_ref_close(target_ref); + if (error) + goto done; + break; + } + + if (!fpa.configs_created && pe != NULL) { + error = create_config_files(fpa.config_info.proto, + fpa.config_info.host, fpa.config_info.port, + fpa.config_info.remote_repo_path, + fpa.config_info.git_url, + fpa.config_info.fetch_all_branches, + fpa.config_info.mirror_references, + fpa.config_info.symrefs, + fpa.config_info.wanted_branches, + fpa.config_info.wanted_refs, fpa.repo); + if (error) + goto done; + } + } + + if (verbosity >= 0) + printf("Created %s repository '%s'\n", + mirror_references ? "mirrored" : "cloned", repo_path); +done: + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + if (fetchpid > 0) { + if (kill(fetchpid, SIGTERM) == -1) + error = got_error_from_errno("kill"); + if (waitpid(fetchpid, &fetchstatus, 0) == -1 && error == NULL) + error = got_error_from_errno("waitpid"); + } + if (fetchfd != -1 && close(fetchfd) == -1 && error == NULL) + error = got_error_from_errno("close"); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + got_pathlist_free(&refs, GOT_PATHLIST_FREE_ALL); + got_pathlist_free(&symrefs, GOT_PATHLIST_FREE_ALL); + got_pathlist_free(&wanted_branches, GOT_PATHLIST_FREE_NONE); + got_pathlist_free(&wanted_refs, GOT_PATHLIST_FREE_NONE); + free(pack_hash); + free(proto); + free(host); + free(port); + free(server_path); + free(repo_name); + free(default_destdir); + free(git_url); + return error; +} + +static const struct got_error * +update_ref(struct got_reference *ref, struct got_object_id *new_id, + int replace_tags, int verbosity, struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *new_id_str = NULL; + struct got_object_id *old_id = NULL; + + err = got_object_id_str(&new_id_str, new_id); + if (err) + goto done; + + if (!replace_tags && + strncmp(got_ref_get_name(ref), "refs/tags/", 10) == 0) { + err = got_ref_resolve(&old_id, repo, ref); + if (err) + goto done; + if (got_object_id_cmp(old_id, new_id) == 0) + goto done; + if (verbosity >= 0) { + printf("Rejecting update of existing tag %s: %s\n", + got_ref_get_name(ref), new_id_str); + } + goto done; + } + + if (got_ref_is_symbolic(ref)) { + if (verbosity >= 0) { + printf("Replacing reference %s: %s\n", + got_ref_get_name(ref), + got_ref_get_symref_target(ref)); + } + err = got_ref_change_symref_to_ref(ref, new_id); + if (err) + goto done; + err = got_ref_write(ref, repo); + if (err) + goto done; + } else { + err = got_ref_resolve(&old_id, repo, ref); + if (err) + goto done; + if (got_object_id_cmp(old_id, new_id) == 0) + goto done; + + err = got_ref_change_ref(ref, new_id); + if (err) + goto done; + err = got_ref_write(ref, repo); + if (err) + goto done; + } + + if (verbosity >= 0) + printf("Updated %s: %s\n", got_ref_get_name(ref), + new_id_str); +done: + free(old_id); + free(new_id_str); + return err; +} + +static const struct got_error * +update_symref(const char *refname, struct got_reference *target_ref, + int verbosity, struct got_repository *repo) +{ + const struct got_error *err = NULL, *unlock_err; + struct got_reference *symref; + int symref_is_locked = 0; + + err = got_ref_open(&symref, repo, refname, 1); + if (err) { + if (err->code != GOT_ERR_NOT_REF) + return err; + err = got_ref_alloc_symref(&symref, refname, target_ref); + if (err) + goto done; + + err = got_ref_write(symref, repo); + if (err) + goto done; + + if (verbosity >= 0) + printf("Created reference %s: %s\n", + got_ref_get_name(symref), + got_ref_get_symref_target(symref)); + } else { + symref_is_locked = 1; + + if (strcmp(got_ref_get_symref_target(symref), + got_ref_get_name(target_ref)) == 0) + goto done; + + err = got_ref_change_symref(symref, + got_ref_get_name(target_ref)); + if (err) + goto done; + + err = got_ref_write(symref, repo); + if (err) + goto done; + + if (verbosity >= 0) + printf("Updated %s: %s\n", got_ref_get_name(symref), + got_ref_get_symref_target(symref)); + + } +done: + if (symref_is_locked) { + unlock_err = got_ref_unlock(symref); + if (unlock_err && err == NULL) + err = unlock_err; + } + got_ref_close(symref); + return err; +} + +__dead static void +usage_fetch(void) +{ + fprintf(stderr, "usage: %s fetch [-adlqtvX] [-b branch] " + "[-R reference] [-r repository-path] [remote-repository]\n", + getprogname()); + exit(1); +} + +static const struct got_error * +delete_missing_ref(struct got_reference *ref, + int verbosity, struct got_repository *repo) +{ + const struct got_error *err = NULL; + struct got_object_id *id = NULL; + char *id_str = NULL; + + if (got_ref_is_symbolic(ref)) { + err = got_ref_delete(ref, repo); + if (err) + return err; + if (verbosity >= 0) { + printf("Deleted %s: %s\n", + got_ref_get_name(ref), + got_ref_get_symref_target(ref)); + } + } else { + err = got_ref_resolve(&id, repo, ref); + if (err) + return err; + err = got_object_id_str(&id_str, id); + if (err) + goto done; + + err = got_ref_delete(ref, repo); + if (err) + goto done; + if (verbosity >= 0) { + printf("Deleted %s: %s\n", + got_ref_get_name(ref), id_str); + } + } +done: + free(id); + free(id_str); + return err; +} + +static const struct got_error * +delete_missing_refs(struct got_pathlist_head *their_refs, + struct got_pathlist_head *their_symrefs, + const struct got_remote_repo *remote, + int verbosity, struct got_repository *repo) +{ + const struct got_error *err = NULL, *unlock_err; + struct got_reflist_head my_refs; + struct got_reflist_entry *re; + struct got_pathlist_entry *pe; + char *remote_namespace = NULL; + char *local_refname = NULL; + + TAILQ_INIT(&my_refs); + + if (asprintf(&remote_namespace, "refs/remotes/%s/", remote->name) + == -1) + return got_error_from_errno("asprintf"); + + err = got_ref_list(&my_refs, repo, NULL, got_ref_cmp_by_name, NULL); + if (err) + goto done; + + TAILQ_FOREACH(re, &my_refs, entry) { + const char *refname = got_ref_get_name(re->ref); + const char *their_refname; + + if (remote->mirror_references) { + their_refname = refname; + } else { + if (strncmp(refname, remote_namespace, + strlen(remote_namespace)) == 0) { + if (strcmp(refname + strlen(remote_namespace), + GOT_REF_HEAD) == 0) + continue; + if (asprintf(&local_refname, "refs/heads/%s", + refname + strlen(remote_namespace)) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + } else if (strncmp(refname, "refs/tags/", 10) != 0) + continue; + + their_refname = local_refname; + } + + TAILQ_FOREACH(pe, their_refs, entry) { + if (strcmp(their_refname, pe->path) == 0) + break; + } + if (pe != NULL) + continue; + + TAILQ_FOREACH(pe, their_symrefs, entry) { + if (strcmp(their_refname, pe->path) == 0) + break; + } + if (pe != NULL) + continue; + + err = delete_missing_ref(re->ref, verbosity, repo); + if (err) + break; + + if (local_refname) { + struct got_reference *ref; + err = got_ref_open(&ref, repo, local_refname, 1); + if (err) { + if (err->code != GOT_ERR_NOT_REF) + break; + free(local_refname); + local_refname = NULL; + continue; + } + err = delete_missing_ref(ref, verbosity, repo); + if (err) + break; + unlock_err = got_ref_unlock(ref); + got_ref_close(ref); + if (unlock_err && err == NULL) { + err = unlock_err; + break; + } + + free(local_refname); + local_refname = NULL; + } + } +done: + got_ref_list_free(&my_refs); + free(remote_namespace); + free(local_refname); + return err; +} + +static const struct got_error * +update_wanted_ref(const char *refname, struct got_object_id *id, + const char *remote_repo_name, int verbosity, struct got_repository *repo) +{ + const struct got_error *err, *unlock_err; + char *remote_refname; + struct got_reference *ref; + + if (strncmp("refs/", refname, 5) == 0) + refname += 5; + + if (asprintf(&remote_refname, "refs/remotes/%s/%s", + remote_repo_name, refname) == -1) + return got_error_from_errno("asprintf"); + + err = got_ref_open(&ref, repo, remote_refname, 1); + if (err) { + if (err->code != GOT_ERR_NOT_REF) + goto done; + err = create_ref(remote_refname, id, verbosity, repo); + } else { + err = update_ref(ref, id, 0, verbosity, repo); + unlock_err = got_ref_unlock(ref); + if (unlock_err && err == NULL) + err = unlock_err; + got_ref_close(ref); + } +done: + free(remote_refname); + return err; +} + +static const struct got_error * +delete_ref(struct got_repository *repo, struct got_reference *ref) +{ + const struct got_error *err = NULL; + struct got_object_id *id = NULL; + char *id_str = NULL; + const char *target; + + if (got_ref_is_symbolic(ref)) { + target = got_ref_get_symref_target(ref); + } else { + err = got_ref_resolve(&id, repo, ref); + if (err) + goto done; + err = got_object_id_str(&id_str, id); + if (err) + goto done; + target = id_str; + } + + err = got_ref_delete(ref, repo); + if (err) + goto done; + + printf("Deleted %s: %s\n", got_ref_get_name(ref), target); +done: + free(id); + free(id_str); + return err; +} + +static const struct got_error * +delete_refs_for_remote(struct got_repository *repo, const char *remote_name) +{ + const struct got_error *err = NULL; + struct got_reflist_head refs; + struct got_reflist_entry *re; + char *prefix; + + TAILQ_INIT(&refs); + + if (asprintf(&prefix, "refs/remotes/%s", remote_name) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + err = got_ref_list(&refs, repo, prefix, got_ref_cmp_by_name, NULL); + if (err) + goto done; + + TAILQ_FOREACH(re, &refs, entry) + delete_ref(repo, re->ref); +done: + got_ref_list_free(&refs); + return err; +} + +static const struct got_error * +cmd_fetch(int argc, char *argv[]) +{ + const struct got_error *error = NULL, *unlock_err; + char *cwd = NULL, *repo_path = NULL; + const char *remote_name; + char *proto = NULL, *host = NULL, *port = NULL; + char *repo_name = NULL, *server_path = NULL; + const struct got_remote_repo *remotes, *remote = NULL; + int nremotes; + char *id_str = NULL; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + const struct got_gotconfig *repo_conf = NULL, *worktree_conf = NULL; + struct got_pathlist_head refs, symrefs, wanted_branches, wanted_refs; + struct got_pathlist_entry *pe; + struct got_reflist_head remote_refs; + struct got_reflist_entry *re; + struct got_object_id *pack_hash = NULL; + int i, ch, fetchfd = -1, fetchstatus; + pid_t fetchpid = -1; + struct got_fetch_progress_arg fpa; + int verbosity = 0, fetch_all_branches = 0, list_refs_only = 0; + int delete_refs = 0, replace_tags = 0, delete_remote = 0; + int *pack_fds = NULL, have_bflag = 0; + const char *remote_head = NULL, *worktree_branch = NULL; + + TAILQ_INIT(&refs); + TAILQ_INIT(&symrefs); + TAILQ_INIT(&remote_refs); + TAILQ_INIT(&wanted_branches); + TAILQ_INIT(&wanted_refs); + + while ((ch = getopt(argc, argv, "ab:dlqR:r:tvX")) != -1) { + switch (ch) { + case 'a': + fetch_all_branches = 1; + break; + case 'b': + error = got_pathlist_append(&wanted_branches, + optarg, NULL); + if (error) + return error; + have_bflag = 1; + break; + case 'd': + delete_refs = 1; + break; + case 'l': + list_refs_only = 1; + break; + case 'q': + verbosity = -1; + break; + case 'R': + error = got_pathlist_append(&wanted_refs, + optarg, NULL); + if (error) + return error; + break; + case 'r': + repo_path = realpath(optarg, NULL); + if (repo_path == NULL) + return got_error_from_errno2("realpath", + optarg); + got_path_strip_trailing_slashes(repo_path); + break; + case 't': + replace_tags = 1; + break; + case 'v': + if (verbosity < 0) + verbosity = 0; + else if (verbosity < 3) + verbosity++; + break; + case 'X': + delete_remote = 1; + break; + default: + usage_fetch(); + break; + } + } + argc -= optind; + argv += optind; + + if (fetch_all_branches && !TAILQ_EMPTY(&wanted_branches)) + option_conflict('a', 'b'); + if (list_refs_only) { + if (!TAILQ_EMPTY(&wanted_branches)) + option_conflict('l', 'b'); + if (fetch_all_branches) + option_conflict('l', 'a'); + if (delete_refs) + option_conflict('l', 'd'); + if (delete_remote) + option_conflict('l', 'X'); + } + if (delete_remote) { + if (fetch_all_branches) + option_conflict('X', 'a'); + if (!TAILQ_EMPTY(&wanted_branches)) + option_conflict('X', 'b'); + if (delete_refs) + option_conflict('X', 'd'); + if (replace_tags) + option_conflict('X', 't'); + if (!TAILQ_EMPTY(&wanted_refs)) + option_conflict('X', 'R'); + } + + if (argc == 0) { + if (delete_remote) + errx(1, "-X option requires a remote name"); + remote_name = GOT_FETCH_DEFAULT_REMOTE_NAME; + } else if (argc == 1) + remote_name = argv[0]; + else + usage_fetch(); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + if (repo_path == NULL) { + error = got_worktree_open(&worktree, cwd); + if (error && error->code != GOT_ERR_NOT_WORKTREE) + goto done; + else + error = NULL; + if (worktree) { + repo_path = + strdup(got_worktree_get_repo_path(worktree)); + if (repo_path == NULL) + error = got_error_from_errno("strdup"); + if (error) + goto done; + } else { + repo_path = strdup(cwd); + if (repo_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + } + + error = got_repo_open(&repo, repo_path, NULL, pack_fds); + if (error) + goto done; + + if (delete_remote) { + error = delete_refs_for_remote(repo, remote_name); + goto done; /* nothing else to do */ + } + + if (worktree) { + worktree_conf = got_worktree_get_gotconfig(worktree); + if (worktree_conf) { + got_gotconfig_get_remotes(&nremotes, &remotes, + worktree_conf); + for (i = 0; i < nremotes; i++) { + if (strcmp(remotes[i].name, remote_name) == 0) { + remote = &remotes[i]; + break; + } + } + } + } + if (remote == NULL) { + repo_conf = got_repo_get_gotconfig(repo); + if (repo_conf) { + got_gotconfig_get_remotes(&nremotes, &remotes, + repo_conf); + for (i = 0; i < nremotes; i++) { + if (strcmp(remotes[i].name, remote_name) == 0) { + remote = &remotes[i]; + break; + } + } + } + } + if (remote == NULL) { + got_repo_get_gitconfig_remotes(&nremotes, &remotes, repo); + for (i = 0; i < nremotes; i++) { + if (strcmp(remotes[i].name, remote_name) == 0) { + remote = &remotes[i]; + break; + } + } + } + if (remote == NULL) { + error = got_error_path(remote_name, GOT_ERR_NO_REMOTE); + goto done; + } + + if (TAILQ_EMPTY(&wanted_branches)) { + if (!fetch_all_branches) + fetch_all_branches = remote->fetch_all_branches; + for (i = 0; i < remote->nfetch_branches; i++) { + error = got_pathlist_append(&wanted_branches, + remote->fetch_branches[i], NULL); + if (error) + goto done; + } + } + if (TAILQ_EMPTY(&wanted_refs)) { + for (i = 0; i < remote->nfetch_refs; i++) { + error = got_pathlist_append(&wanted_refs, + remote->fetch_refs[i], NULL); + if (error) + goto done; + } + } + + error = got_dial_parse_uri(&proto, &host, &port, &server_path, + &repo_name, remote->fetch_url); + if (error) + goto done; + + if (strcmp(proto, "git") == 0) { +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec " + "sendfd dns inet unveil", NULL) == -1) + err(1, "pledge"); +#endif + } else if (strcmp(proto, "git+ssh") == 0 || + strcmp(proto, "ssh") == 0) { +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec " + "sendfd unveil", NULL) == -1) + err(1, "pledge"); +#endif + } else if (strcmp(proto, "http") == 0 || + strcmp(proto, "git+http") == 0) { + error = got_error_path(proto, GOT_ERR_NOT_IMPL); + goto done; + } else { + error = got_error_path(proto, GOT_ERR_BAD_PROTO); + goto done; + } + + error = got_dial_apply_unveil(proto); + if (error) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 0, NULL); + if (error) + goto done; + + if (verbosity >= 0) { + printf("Connecting to \"%s\" %s://%s%s%s%s%s\n", + remote->name, proto, host, + port ? ":" : "", port ? port : "", + *server_path == '/' ? "" : "/", server_path); + } + + error = got_fetch_connect(&fetchpid, &fetchfd, proto, host, port, + server_path, verbosity); + if (error) + goto done; + + if (!have_bflag) { + /* + * If set, get this remote's HEAD ref target so + * if it has changed on the server we can fetch it. + */ + error = got_ref_list(&remote_refs, repo, "refs/remotes", + got_ref_cmp_by_name, repo); + if (error) + goto done; + + TAILQ_FOREACH(re, &remote_refs, entry) { + const char *remote_refname, *remote_target; + size_t remote_name_len; + + if (!got_ref_is_symbolic(re->ref)) + continue; + + remote_name_len = strlen(remote->name); + remote_refname = got_ref_get_name(re->ref); + + /* we only want refs/remotes/$remote->name/HEAD */ + if (strncmp(remote_refname + 13, remote->name, + remote_name_len) != 0) + continue; + + if (strcmp(remote_refname + remote_name_len + 14, + GOT_REF_HEAD) != 0) + continue; + + /* + * Take the name itself because we already + * only match with refs/heads/ in fetch_pack(). + */ + remote_target = got_ref_get_symref_target(re->ref); + remote_head = remote_target + remote_name_len + 14; + break; + } + + if (worktree) { + const char *refname; + + refname = got_worktree_get_head_ref_name(worktree); + if (strncmp(refname, "refs/heads/", 11) == 0) + worktree_branch = refname; + } + } + + fpa.last_scaled_size[0] = '\0'; + fpa.last_p_indexed = -1; + fpa.last_p_resolved = -1; + fpa.verbosity = verbosity; + fpa.repo = repo; + fpa.create_configs = 0; + fpa.configs_created = 0; + memset(&fpa.config_info, 0, sizeof(fpa.config_info)); + + error = got_fetch_pack(&pack_hash, &refs, &symrefs, remote->name, + remote->mirror_references, fetch_all_branches, &wanted_branches, + &wanted_refs, list_refs_only, verbosity, fetchfd, repo, + worktree_branch, remote_head, have_bflag, fetch_progress, &fpa); + if (error) + goto done; + + if (list_refs_only) { + error = list_remote_refs(&symrefs, &refs); + goto done; + } + + if (pack_hash == NULL) { + if (verbosity >= 0) + printf("Already up-to-date\n"); + } else if (verbosity >= 0) { + error = got_object_id_str(&id_str, pack_hash); + if (error) + goto done; + printf("\nFetched %s.pack\n", id_str); + free(id_str); + id_str = NULL; + } + + /* Update references provided with the pack file. */ + TAILQ_FOREACH(pe, &refs, entry) { + const char *refname = pe->path; + struct got_object_id *id = pe->data; + struct got_reference *ref; + char *remote_refname; + + if (is_wanted_ref(&wanted_refs, refname) && + !remote->mirror_references) { + error = update_wanted_ref(refname, id, + remote->name, verbosity, repo); + if (error) + goto done; + continue; + } + + if (remote->mirror_references || + strncmp("refs/tags/", refname, 10) == 0) { + error = got_ref_open(&ref, repo, refname, 1); + if (error) { + if (error->code != GOT_ERR_NOT_REF) + goto done; + error = create_ref(refname, id, verbosity, + repo); + if (error) + goto done; + } else { + error = update_ref(ref, id, replace_tags, + verbosity, repo); + unlock_err = got_ref_unlock(ref); + if (unlock_err && error == NULL) + error = unlock_err; + got_ref_close(ref); + if (error) + goto done; + } + } else if (strncmp("refs/heads/", refname, 11) == 0) { + if (asprintf(&remote_refname, "refs/remotes/%s/%s", + remote_name, refname + 11) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + + error = got_ref_open(&ref, repo, remote_refname, 1); + if (error) { + if (error->code != GOT_ERR_NOT_REF) + goto done; + error = create_ref(remote_refname, id, + verbosity, repo); + if (error) + goto done; + } else { + error = update_ref(ref, id, replace_tags, + verbosity, repo); + unlock_err = got_ref_unlock(ref); + if (unlock_err && error == NULL) + error = unlock_err; + got_ref_close(ref); + if (error) + goto done; + } + + /* Also create a local branch if none exists yet. */ + error = got_ref_open(&ref, repo, refname, 1); + if (error) { + if (error->code != GOT_ERR_NOT_REF) + goto done; + error = create_ref(refname, id, verbosity, + repo); + if (error) + goto done; + } else { + unlock_err = got_ref_unlock(ref); + if (unlock_err && error == NULL) + error = unlock_err; + got_ref_close(ref); + } + } + } + if (delete_refs) { + error = delete_missing_refs(&refs, &symrefs, remote, + verbosity, repo); + if (error) + goto done; + } + + if (!remote->mirror_references) { + /* Update remote HEAD reference if the server provided one. */ + TAILQ_FOREACH(pe, &symrefs, entry) { + struct got_reference *target_ref; + const char *refname = pe->path; + const char *target = pe->data; + char *remote_refname = NULL, *remote_target = NULL; + + if (strcmp(refname, GOT_REF_HEAD) != 0) + continue; + + if (strncmp("refs/heads/", target, 11) != 0) + continue; + + if (asprintf(&remote_refname, "refs/remotes/%s/%s", + remote->name, refname) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + if (asprintf(&remote_target, "refs/remotes/%s/%s", + remote->name, target + 11) == -1) { + error = got_error_from_errno("asprintf"); + free(remote_refname); + goto done; + } + + error = got_ref_open(&target_ref, repo, remote_target, + 0); + if (error) { + free(remote_refname); + free(remote_target); + if (error->code == GOT_ERR_NOT_REF) { + error = NULL; + continue; + } + goto done; + } + error = update_symref(remote_refname, target_ref, + verbosity, repo); + free(remote_refname); + free(remote_target); + got_ref_close(target_ref); + if (error) + goto done; + } + } +done: + if (fetchpid > 0) { + if (kill(fetchpid, SIGTERM) == -1) + error = got_error_from_errno("kill"); + if (waitpid(fetchpid, &fetchstatus, 0) == -1 && error == NULL) + error = got_error_from_errno("waitpid"); + } + if (fetchfd != -1 && close(fetchfd) == -1 && error == NULL) + error = got_error_from_errno("close"); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (worktree) + got_worktree_close(worktree); + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + got_pathlist_free(&refs, GOT_PATHLIST_FREE_ALL); + got_pathlist_free(&symrefs, GOT_PATHLIST_FREE_ALL); + got_pathlist_free(&wanted_branches, GOT_PATHLIST_FREE_NONE); + got_pathlist_free(&wanted_refs, GOT_PATHLIST_FREE_NONE); + got_ref_list_free(&remote_refs); + free(id_str); + free(cwd); + free(repo_path); + free(pack_hash); + free(proto); + free(host); + free(port); + free(server_path); + free(repo_name); + return error; +} + + +__dead static void +usage_checkout(void) +{ + fprintf(stderr, "usage: %s checkout [-Eq] [-b branch] [-c commit] " + "[-p path-prefix] repository-path [work-tree-path]\n", + getprogname()); + exit(1); +} + +static void +show_worktree_base_ref_warning(void) +{ + fprintf(stderr, "%s: warning: could not create a reference " + "to the work tree's base commit; the commit could be " + "garbage-collected by Git or 'gotadmin cleanup'; making the " + "repository writable and running 'got update' will prevent this\n", + getprogname()); +} + +struct got_checkout_progress_arg { + const char *worktree_path; + int had_base_commit_ref_error; + int verbosity; +}; + +static const struct got_error * +checkout_progress(void *arg, unsigned char status, const char *path) +{ + struct got_checkout_progress_arg *a = arg; + + /* Base commit bump happens silently. */ + if (status == GOT_STATUS_BUMP_BASE) + return NULL; + + if (status == GOT_STATUS_BASE_REF_ERR) { + a->had_base_commit_ref_error = 1; + return NULL; + } + + while (path[0] == '/') + path++; + + if (a->verbosity >= 0) + printf("%c %s/%s\n", status, a->worktree_path, path); + + return NULL; +} + +static const struct got_error * +check_cancelled(void *arg) +{ + if (sigint_received || sigpipe_received) + return got_error(GOT_ERR_CANCELLED); + return NULL; +} + +static const struct got_error * +check_linear_ancestry(struct got_object_id *commit_id, + struct got_object_id *base_commit_id, int allow_forwards_in_time_only, + struct got_repository *repo) +{ + const struct got_error *err = NULL; + struct got_object_id *yca_id; + + err = got_commit_graph_find_youngest_common_ancestor(&yca_id, + commit_id, base_commit_id, 1, repo, check_cancelled, NULL); + if (err) + return err; + + if (yca_id == NULL) + return got_error(GOT_ERR_ANCESTRY); + + /* + * Require a straight line of history between the target commit + * and the work tree's base commit. + * + * Non-linear situations such as this require a rebase: + * + * (commit) D F (base_commit) + * \ / + * C E + * \ / + * B (yca) + * | + * A + * + * 'got update' only handles linear cases: + * Update forwards in time: A (base/yca) - B - C - D (commit) + * Update backwards in time: D (base) - C - B - A (commit/yca) + */ + if (allow_forwards_in_time_only) { + if (got_object_id_cmp(base_commit_id, yca_id) != 0) + return got_error(GOT_ERR_ANCESTRY); + } else if (got_object_id_cmp(commit_id, yca_id) != 0 && + got_object_id_cmp(base_commit_id, yca_id) != 0) + return got_error(GOT_ERR_ANCESTRY); + + free(yca_id); + return NULL; +} + +static const struct got_error * +check_same_branch(struct got_object_id *commit_id, + struct got_reference *head_ref, struct got_repository *repo) +{ + const struct got_error *err = NULL; + struct got_commit_graph *graph = NULL; + struct got_object_id *head_commit_id = NULL; + + err = got_ref_resolve(&head_commit_id, repo, head_ref); + if (err) + goto done; + + if (got_object_id_cmp(head_commit_id, commit_id) == 0) + goto done; + + err = got_commit_graph_open(&graph, "/", 1); + if (err) + goto done; + + err = got_commit_graph_iter_start(graph, head_commit_id, repo, + check_cancelled, NULL); + if (err) + goto done; + + for (;;) { + struct got_object_id id; + + err = got_commit_graph_iter_next(&id, graph, repo, + check_cancelled, NULL); + if (err) { + if (err->code == GOT_ERR_ITER_COMPLETED) + err = got_error(GOT_ERR_ANCESTRY); + break; + } + + if (got_object_id_cmp(&id, commit_id) == 0) + break; + } +done: + if (graph) + got_commit_graph_close(graph); + free(head_commit_id); + return err; +} + +static const struct got_error * +checkout_ancestry_error(struct got_reference *ref, const char *commit_id_str) +{ + static char msg[512]; + const char *branch_name; + + if (got_ref_is_symbolic(ref)) + branch_name = got_ref_get_symref_target(ref); + else + branch_name = got_ref_get_name(ref); + + if (strncmp("refs/heads/", branch_name, 11) == 0) + branch_name += 11; + + snprintf(msg, sizeof(msg), + "target commit is not contained in branch '%s'; " + "the branch to use must be specified with -b; " + "if necessary a new branch can be created for " + "this commit with 'got branch -c %s BRANCH_NAME'", + branch_name, commit_id_str); + + return got_error_msg(GOT_ERR_ANCESTRY, msg); +} + +static const struct got_error * +cmd_checkout(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_repository *repo = NULL; + struct got_reference *head_ref = NULL, *ref = NULL; + struct got_worktree *worktree = NULL; + char *repo_path = NULL; + char *worktree_path = NULL; + const char *path_prefix = ""; + const char *branch_name = GOT_REF_HEAD, *refname = NULL; + char *commit_id_str = NULL; + struct got_object_id *commit_id = NULL; + char *cwd = NULL; + int ch, same_path_prefix, allow_nonempty = 0, verbosity = 0; + struct got_pathlist_head paths; + struct got_checkout_progress_arg cpa; + int *pack_fds = NULL; + + TAILQ_INIT(&paths); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "b:c:Ep:q")) != -1) { + switch (ch) { + case 'b': + branch_name = optarg; + break; + case 'c': + commit_id_str = strdup(optarg); + if (commit_id_str == NULL) + return got_error_from_errno("strdup"); + break; + case 'E': + allow_nonempty = 1; + break; + case 'p': + path_prefix = optarg; + break; + case 'q': + verbosity = -1; + break; + default: + usage_checkout(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (argc == 1) { + char *base, *dotgit; + const char *path; + repo_path = realpath(argv[0], NULL); + if (repo_path == NULL) + return got_error_from_errno2("realpath", argv[0]); + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + if (path_prefix[0]) + path = path_prefix; + else + path = repo_path; + error = got_path_basename(&base, path); + if (error) + goto done; + dotgit = strstr(base, ".git"); + if (dotgit) + *dotgit = '\0'; + if (asprintf(&worktree_path, "%s/%s", cwd, base) == -1) { + error = got_error_from_errno("asprintf"); + free(base); + goto done; + } + free(base); + } else if (argc == 2) { + repo_path = realpath(argv[0], NULL); + if (repo_path == NULL) { + error = got_error_from_errno2("realpath", argv[0]); + goto done; + } + worktree_path = realpath(argv[1], NULL); + if (worktree_path == NULL) { + if (errno != ENOENT) { + error = got_error_from_errno2("realpath", + argv[1]); + goto done; + } + worktree_path = strdup(argv[1]); + if (worktree_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + } else + usage_checkout(); + + got_path_strip_trailing_slashes(repo_path); + got_path_strip_trailing_slashes(worktree_path); + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_repo_open(&repo, repo_path, NULL, pack_fds); + if (error != NULL) + goto done; + + /* Pre-create work tree path for unveil(2) */ + error = got_path_mkdir(worktree_path); + if (error) { + if (!(error->code == GOT_ERR_ERRNO && errno == EISDIR) && + !(error->code == GOT_ERR_ERRNO && errno == EEXIST)) + goto done; + if (!allow_nonempty && + !got_path_dir_is_empty(worktree_path)) { + error = got_error_path(worktree_path, + GOT_ERR_DIR_NOT_EMPTY); + goto done; + } + } + + error = apply_unveil(got_repo_get_path(repo), 0, worktree_path); + if (error) + goto done; + + error = got_ref_open(&head_ref, repo, branch_name, 0); + if (error != NULL) + goto done; + + error = got_worktree_init(worktree_path, head_ref, path_prefix, repo); + if (error != NULL && !(error->code == GOT_ERR_ERRNO && errno == EEXIST)) + goto done; + + error = got_worktree_open(&worktree, worktree_path); + if (error != NULL) + goto done; + + error = got_worktree_match_path_prefix(&same_path_prefix, worktree, + path_prefix); + if (error != NULL) + goto done; + if (!same_path_prefix) { + error = got_error(GOT_ERR_PATH_PREFIX); + goto done; + } + + if (commit_id_str) { + struct got_reflist_head refs; + TAILQ_INIT(&refs); + error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, + NULL); + if (error) + goto done; + error = got_repo_match_object_id(&commit_id, NULL, + commit_id_str, GOT_OBJ_TYPE_COMMIT, &refs, repo); + got_ref_list_free(&refs); + if (error) + goto done; + error = check_linear_ancestry(commit_id, + got_worktree_get_base_commit_id(worktree), 0, repo); + if (error != NULL) { + if (error->code == GOT_ERR_ANCESTRY) { + error = checkout_ancestry_error( + head_ref, commit_id_str); + } + goto done; + } + error = check_same_branch(commit_id, head_ref, repo); + if (error) { + if (error->code == GOT_ERR_ANCESTRY) { + error = checkout_ancestry_error( + head_ref, commit_id_str); + } + goto done; + } + error = got_worktree_set_base_commit_id(worktree, repo, + commit_id); + if (error) + goto done; + /* Expand potentially abbreviated commit ID string. */ + free(commit_id_str); + error = got_object_id_str(&commit_id_str, commit_id); + if (error) + goto done; + } else { + commit_id = got_object_id_dup( + got_worktree_get_base_commit_id(worktree)); + if (commit_id == NULL) { + error = got_error_from_errno("got_object_id_dup"); + goto done; + } + error = got_object_id_str(&commit_id_str, commit_id); + if (error) + goto done; + } + + error = got_pathlist_append(&paths, "", NULL); + if (error) + goto done; + cpa.worktree_path = worktree_path; + cpa.had_base_commit_ref_error = 0; + cpa.verbosity = verbosity; + error = got_worktree_checkout_files(worktree, &paths, repo, + checkout_progress, &cpa, check_cancelled, NULL); + if (error != NULL) + goto done; + + if (got_ref_is_symbolic(head_ref)) { + error = got_ref_resolve_symbolic(&ref, repo, head_ref); + if (error) + goto done; + refname = got_ref_get_name(ref); + } else + refname = got_ref_get_name(head_ref); + printf("Checked out %s: %s\n", refname, commit_id_str); + printf("Now shut up and hack\n"); + if (cpa.had_base_commit_ref_error) + show_worktree_base_ref_warning(); +done: + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + if (head_ref) + got_ref_close(head_ref); + if (ref) + got_ref_close(ref); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + got_pathlist_free(&paths, GOT_PATHLIST_FREE_NONE); + free(commit_id_str); + free(commit_id); + free(repo_path); + free(worktree_path); + free(cwd); + return error; +} + +struct got_update_progress_arg { + int did_something; + int conflicts; + int obstructed; + int not_updated; + int missing; + int not_deleted; + int unversioned; + int verbosity; +}; + +static void +print_update_progress_stats(struct got_update_progress_arg *upa) +{ + if (!upa->did_something) + return; + + if (upa->conflicts > 0) + printf("Files with new merge conflicts: %d\n", upa->conflicts); + if (upa->obstructed > 0) + printf("File paths obstructed by a non-regular file: %d\n", + upa->obstructed); + if (upa->not_updated > 0) + printf("Files not updated because of existing merge " + "conflicts: %d\n", upa->not_updated); +} + +/* + * The meaning of some status codes differs between merge-style operations and + * update operations. For example, the ! status code means "file was missing" + * if changes were merged into the work tree, and "missing file was restored" + * if the work tree was updated. This function should be used by any operation + * which merges changes into the work tree without updating the work tree. + */ +static void +print_merge_progress_stats(struct got_update_progress_arg *upa) +{ + if (!upa->did_something) + return; + + if (upa->conflicts > 0) + printf("Files with new merge conflicts: %d\n", upa->conflicts); + if (upa->obstructed > 0) + printf("File paths obstructed by a non-regular file: %d\n", + upa->obstructed); + if (upa->missing > 0) + printf("Files which had incoming changes but could not be " + "found in the work tree: %d\n", upa->missing); + if (upa->not_deleted > 0) + printf("Files not deleted due to differences in deleted " + "content: %d\n", upa->not_deleted); + if (upa->unversioned > 0) + printf("Files not merged because an unversioned file was " + "found in the work tree: %d\n", upa->unversioned); +} + +__dead static void +usage_update(void) +{ + fprintf(stderr, "usage: %s update [-q] [-b branch] [-c commit] " + "[path ...]\n", getprogname()); + exit(1); +} + +static const struct got_error * +update_progress(void *arg, unsigned char status, const char *path) +{ + struct got_update_progress_arg *upa = arg; + + if (status == GOT_STATUS_EXISTS || + status == GOT_STATUS_BASE_REF_ERR) + return NULL; + + upa->did_something = 1; + + /* Base commit bump happens silently. */ + if (status == GOT_STATUS_BUMP_BASE) + return NULL; + + if (status == GOT_STATUS_CONFLICT) + upa->conflicts++; + if (status == GOT_STATUS_OBSTRUCTED) + upa->obstructed++; + if (status == GOT_STATUS_CANNOT_UPDATE) + upa->not_updated++; + if (status == GOT_STATUS_MISSING) + upa->missing++; + if (status == GOT_STATUS_CANNOT_DELETE) + upa->not_deleted++; + if (status == GOT_STATUS_UNVERSIONED) + upa->unversioned++; + + while (path[0] == '/') + path++; + if (upa->verbosity >= 0) + printf("%c %s\n", status, path); + + return NULL; +} + +static const struct got_error * +switch_head_ref(struct got_reference *head_ref, + struct got_object_id *commit_id, struct got_worktree *worktree, + struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *base_id_str; + int ref_has_moved = 0; + + /* Trivial case: switching between two different references. */ + if (strcmp(got_ref_get_name(head_ref), + got_worktree_get_head_ref_name(worktree)) != 0) { + printf("Switching work tree from %s to %s\n", + got_worktree_get_head_ref_name(worktree), + got_ref_get_name(head_ref)); + return got_worktree_set_head_ref(worktree, head_ref); + } + + err = check_linear_ancestry(commit_id, + got_worktree_get_base_commit_id(worktree), 0, repo); + if (err) { + if (err->code != GOT_ERR_ANCESTRY) + return err; + ref_has_moved = 1; + } + if (!ref_has_moved) + return NULL; + + /* Switching to a rebased branch with the same reference name. */ + err = got_object_id_str(&base_id_str, + got_worktree_get_base_commit_id(worktree)); + if (err) + return err; + printf("Reference %s now points at a different branch\n", + got_worktree_get_head_ref_name(worktree)); + printf("Switching work tree from %s to %s\n", base_id_str, + got_worktree_get_head_ref_name(worktree)); + return NULL; +} + +static const struct got_error * +check_rebase_or_histedit_in_progress(struct got_worktree *worktree) +{ + const struct got_error *err; + int in_progress; + + err = got_worktree_rebase_in_progress(&in_progress, worktree); + if (err) + return err; + if (in_progress) + return got_error(GOT_ERR_REBASING); + + err = got_worktree_histedit_in_progress(&in_progress, worktree); + if (err) + return err; + if (in_progress) + return got_error(GOT_ERR_HISTEDIT_BUSY); + + return NULL; +} + +static const struct got_error * +check_merge_in_progress(struct got_worktree *worktree, + struct got_repository *repo) +{ + const struct got_error *err; + int in_progress; + + err = got_worktree_merge_in_progress(&in_progress, worktree, repo); + if (err) + return err; + if (in_progress) + return got_error(GOT_ERR_MERGE_BUSY); + + return NULL; +} + +static const struct got_error * +get_worktree_paths_from_argv(struct got_pathlist_head *paths, int argc, + char *argv[], struct got_worktree *worktree) +{ + const struct got_error *err = NULL; + char *path; + struct got_pathlist_entry *new; + int i; + + if (argc == 0) { + path = strdup(""); + if (path == NULL) + return got_error_from_errno("strdup"); + return got_pathlist_append(paths, path, NULL); + } + + for (i = 0; i < argc; i++) { + err = got_worktree_resolve_path(&path, worktree, argv[i]); + if (err) + break; + err = got_pathlist_insert(&new, paths, path, NULL); + if (err || new == NULL /* duplicate */) { + free(path); + if (err) + break; + } + } + + return err; +} + +static const struct got_error * +wrap_not_worktree_error(const struct got_error *orig_err, + const char *cmdname, const char *path) +{ + const struct got_error *err; + struct got_repository *repo; + static char msg[512]; + int *pack_fds = NULL; + + err = got_repo_pack_fds_open(&pack_fds); + if (err) + return err; + + err = got_repo_open(&repo, path, NULL, pack_fds); + if (err) + return orig_err; + + snprintf(msg, sizeof(msg), + "'got %s' needs a work tree in addition to a git repository\n" + "Work trees can be checked out from this Git repository with " + "'got checkout'.\n" + "The got(1) manual page contains more information.", cmdname); + err = got_error_msg(GOT_ERR_NOT_WORKTREE, msg); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (err == NULL) + err = close_err; + } + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (err == NULL) + err = pack_err; + } + return err; +} + +static const struct got_error * +cmd_update(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + char *worktree_path = NULL; + struct got_object_id *commit_id = NULL; + char *commit_id_str = NULL; + const char *branch_name = NULL; + struct got_reference *head_ref = NULL; + struct got_pathlist_head paths; + struct got_pathlist_entry *pe; + int ch, verbosity = 0; + struct got_update_progress_arg upa; + int *pack_fds = NULL; + + TAILQ_INIT(&paths); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "b:c:q")) != -1) { + switch (ch) { + case 'b': + branch_name = optarg; + break; + case 'c': + commit_id_str = strdup(optarg); + if (commit_id_str == NULL) + return got_error_from_errno("strdup"); + break; + case 'q': + verbosity = -1; + break; + default: + usage_update(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + worktree_path = getcwd(NULL, 0); + if (worktree_path == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_open(&worktree, worktree_path); + if (error) { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, "update", + worktree_path); + goto done; + } + + error = check_rebase_or_histedit_in_progress(worktree); + if (error) + goto done; + + error = got_repo_open(&repo, got_worktree_get_repo_path(worktree), + NULL, pack_fds); + if (error != NULL) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 0, + got_worktree_get_root_path(worktree)); + if (error) + goto done; + + error = check_merge_in_progress(worktree, repo); + if (error) + goto done; + + error = get_worktree_paths_from_argv(&paths, argc, argv, worktree); + if (error) + goto done; + + error = got_ref_open(&head_ref, repo, branch_name ? branch_name : + got_worktree_get_head_ref_name(worktree), 0); + if (error != NULL) + goto done; + if (commit_id_str == NULL) { + error = got_ref_resolve(&commit_id, repo, head_ref); + if (error != NULL) + goto done; + error = got_object_id_str(&commit_id_str, commit_id); + if (error != NULL) + goto done; + } else { + struct got_reflist_head refs; + TAILQ_INIT(&refs); + error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, + NULL); + if (error) + goto done; + error = got_repo_match_object_id(&commit_id, NULL, + commit_id_str, GOT_OBJ_TYPE_COMMIT, &refs, repo); + got_ref_list_free(&refs); + free(commit_id_str); + commit_id_str = NULL; + if (error) + goto done; + error = got_object_id_str(&commit_id_str, commit_id); + if (error) + goto done; + } + + if (branch_name) { + struct got_object_id *head_commit_id; + TAILQ_FOREACH(pe, &paths, entry) { + if (pe->path_len == 0) + continue; + error = got_error_msg(GOT_ERR_BAD_PATH, + "switching between branches requires that " + "the entire work tree gets updated"); + goto done; + } + error = got_ref_resolve(&head_commit_id, repo, head_ref); + if (error) + goto done; + error = check_linear_ancestry(commit_id, head_commit_id, 0, + repo); + free(head_commit_id); + if (error != NULL) + goto done; + error = check_same_branch(commit_id, head_ref, repo); + if (error) + goto done; + error = switch_head_ref(head_ref, commit_id, worktree, repo); + if (error) + goto done; + } else { + error = check_linear_ancestry(commit_id, + got_worktree_get_base_commit_id(worktree), 0, repo); + if (error != NULL) { + if (error->code == GOT_ERR_ANCESTRY) + error = got_error(GOT_ERR_BRANCH_MOVED); + goto done; + } + error = check_same_branch(commit_id, head_ref, repo); + if (error) + goto done; + } + + if (got_object_id_cmp(got_worktree_get_base_commit_id(worktree), + commit_id) != 0) { + error = got_worktree_set_base_commit_id(worktree, repo, + commit_id); + if (error) + goto done; + } + + memset(&upa, 0, sizeof(upa)); + upa.verbosity = verbosity; + error = got_worktree_checkout_files(worktree, &paths, repo, + update_progress, &upa, check_cancelled, NULL); + if (error != NULL) + goto done; + + if (upa.did_something) { + printf("Updated to %s: %s\n", + got_worktree_get_head_ref_name(worktree), commit_id_str); + } else + printf("Already up-to-date\n"); + + print_update_progress_stats(&upa); +done: + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + free(worktree_path); + got_pathlist_free(&paths, GOT_PATHLIST_FREE_PATH); + free(commit_id); + free(commit_id_str); + return error; +} + +static const struct got_error * +diff_blobs(struct got_object_id *blob_id1, struct got_object_id *blob_id2, + const char *path, int diff_context, int ignore_whitespace, + int force_text_diff, struct got_diffstat_cb_arg *dsa, + struct got_repository *repo, FILE *outfile) +{ + const struct got_error *err = NULL; + struct got_blob_object *blob1 = NULL, *blob2 = NULL; + FILE *f1 = NULL, *f2 = NULL; + int fd1 = -1, fd2 = -1; + + fd1 = got_opentempfd(); + if (fd1 == -1) + return got_error_from_errno("got_opentempfd"); + fd2 = got_opentempfd(); + if (fd2 == -1) { + err = got_error_from_errno("got_opentempfd"); + goto done; + } + + if (blob_id1) { + err = got_object_open_as_blob(&blob1, repo, blob_id1, 8192, + fd1); + if (err) + goto done; + } + + err = got_object_open_as_blob(&blob2, repo, blob_id2, 8192, fd2); + if (err) + goto done; + + f1 = got_opentemp(); + if (f1 == NULL) { + err = got_error_from_errno("got_opentemp"); + goto done; + } + f2 = got_opentemp(); + if (f2 == NULL) { + err = got_error_from_errno("got_opentemp"); + goto done; + } + + while (path[0] == '/') + path++; + err = got_diff_blob(NULL, NULL, blob1, blob2, f1, f2, path, path, + GOT_DIFF_ALGORITHM_PATIENCE, diff_context, ignore_whitespace, + force_text_diff, dsa, outfile); +done: + if (fd1 != -1 && close(fd1) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (blob1) + got_object_blob_close(blob1); + if (fd2 != -1 && close(fd2) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (blob2) + got_object_blob_close(blob2); + if (f1 && fclose(f1) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + if (f2 && fclose(f2) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + return err; +} + +static const struct got_error * +diff_trees(struct got_object_id *tree_id1, struct got_object_id *tree_id2, + const char *path, int diff_context, int ignore_whitespace, + int force_text_diff, struct got_diffstat_cb_arg *dsa, + struct got_repository *repo, FILE *outfile) +{ + const struct got_error *err = NULL; + struct got_tree_object *tree1 = NULL, *tree2 = NULL; + struct got_diff_blob_output_unidiff_arg arg; + FILE *f1 = NULL, *f2 = NULL; + int fd1 = -1, fd2 = -1; + + if (tree_id1) { + err = got_object_open_as_tree(&tree1, repo, tree_id1); + if (err) + goto done; + fd1 = got_opentempfd(); + if (fd1 == -1) { + err = got_error_from_errno("got_opentempfd"); + goto done; + } + } + + err = got_object_open_as_tree(&tree2, repo, tree_id2); + if (err) + goto done; + + f1 = got_opentemp(); + if (f1 == NULL) { + err = got_error_from_errno("got_opentemp"); + goto done; + } + + f2 = got_opentemp(); + if (f2 == NULL) { + err = got_error_from_errno("got_opentemp"); + goto done; + } + fd2 = got_opentempfd(); + if (fd2 == -1) { + err = got_error_from_errno("got_opentempfd"); + goto done; + } + arg.diff_context = diff_context; + arg.ignore_whitespace = ignore_whitespace; + arg.force_text_diff = force_text_diff; + arg.diffstat = dsa; + arg.diff_algo = GOT_DIFF_ALGORITHM_PATIENCE; + arg.outfile = outfile; + arg.lines = NULL; + arg.nlines = 0; + while (path[0] == '/') + path++; + err = got_diff_tree(tree1, tree2, f1, f2, fd1, fd2, path, path, repo, + got_diff_blob_output_unidiff, &arg, 1); +done: + if (tree1) + got_object_tree_close(tree1); + if (tree2) + got_object_tree_close(tree2); + if (f1 && fclose(f1) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + if (f2 && fclose(f2) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + if (fd1 != -1 && close(fd1) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (fd2 != -1 && close(fd2) == -1 && err == NULL) + err = got_error_from_errno("close"); + return err; +} + +static const struct got_error * +get_changed_paths(struct got_pathlist_head *paths, + struct got_commit_object *commit, struct got_repository *repo, + struct got_diffstat_cb_arg *dsa) +{ + const struct got_error *err = NULL; + struct got_object_id *tree_id1 = NULL, *tree_id2 = NULL; + struct got_tree_object *tree1 = NULL, *tree2 = NULL; + struct got_object_qid *qid; + got_diff_blob_cb cb = got_diff_tree_collect_changed_paths; + FILE *f1 = NULL, *f2 = NULL; + int fd1 = -1, fd2 = -1; + + if (dsa) { + cb = got_diff_tree_compute_diffstat; + + f1 = got_opentemp(); + if (f1 == NULL) { + err = got_error_from_errno("got_opentemp"); + goto done; + } + f2 = got_opentemp(); + if (f2 == NULL) { + err = got_error_from_errno("got_opentemp"); + goto done; + } + fd1 = got_opentempfd(); + if (fd1 == -1) { + err = got_error_from_errno("got_opentempfd"); + goto done; + } + fd2 = got_opentempfd(); + if (fd2 == -1) { + err = got_error_from_errno("got_opentempfd"); + goto done; + } + } + + qid = STAILQ_FIRST(got_object_commit_get_parent_ids(commit)); + if (qid != NULL) { + struct got_commit_object *pcommit; + err = got_object_open_as_commit(&pcommit, repo, + &qid->id); + if (err) + return err; + + tree_id1 = got_object_id_dup( + got_object_commit_get_tree_id(pcommit)); + if (tree_id1 == NULL) { + got_object_commit_close(pcommit); + return got_error_from_errno("got_object_id_dup"); + } + got_object_commit_close(pcommit); + + } + + if (tree_id1) { + err = got_object_open_as_tree(&tree1, repo, tree_id1); + if (err) + goto done; + } + + tree_id2 = got_object_commit_get_tree_id(commit); + err = got_object_open_as_tree(&tree2, repo, tree_id2); + if (err) + goto done; + + err = got_diff_tree(tree1, tree2, f1, f2, fd1, fd2, "", "", repo, + cb, dsa ? (void *)dsa : paths, dsa ? 1 : 0); +done: + if (tree1) + got_object_tree_close(tree1); + if (tree2) + got_object_tree_close(tree2); + if (fd1 != -1 && close(fd1) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (fd2 != -1 && close(fd2) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (f1 && fclose(f1) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + if (f2 && fclose(f2) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + free(tree_id1); + return err; +} + +static const struct got_error * +print_patch(struct got_commit_object *commit, struct got_object_id *id, + const char *path, int diff_context, struct got_diffstat_cb_arg *dsa, + struct got_repository *repo, FILE *outfile) +{ + const struct got_error *err = NULL; + struct got_commit_object *pcommit = NULL; + char *id_str1 = NULL, *id_str2 = NULL; + struct got_object_id *obj_id1 = NULL, *obj_id2 = NULL; + struct got_object_qid *qid; + + qid = STAILQ_FIRST(got_object_commit_get_parent_ids(commit)); + if (qid != NULL) { + err = got_object_open_as_commit(&pcommit, repo, + &qid->id); + if (err) + return err; + err = got_object_id_str(&id_str1, &qid->id); + if (err) + goto done; + } + + err = got_object_id_str(&id_str2, id); + if (err) + goto done; + + if (path && path[0] != '\0') { + int obj_type; + err = got_object_id_by_path(&obj_id2, repo, commit, path); + if (err) + goto done; + if (pcommit) { + err = got_object_id_by_path(&obj_id1, repo, + pcommit, path); + if (err) { + if (err->code != GOT_ERR_NO_TREE_ENTRY) { + free(obj_id2); + goto done; + } + } + } + err = got_object_get_type(&obj_type, repo, obj_id2); + if (err) { + free(obj_id2); + goto done; + } + fprintf(outfile, + "diff %s %s\n", id_str1 ? id_str1 : "/dev/null", id_str2); + fprintf(outfile, "commit - %s\n", + id_str1 ? id_str1 : "/dev/null"); + fprintf(outfile, "commit + %s\n", id_str2); + switch (obj_type) { + case GOT_OBJ_TYPE_BLOB: + err = diff_blobs(obj_id1, obj_id2, path, diff_context, + 0, 0, dsa, repo, outfile); + break; + case GOT_OBJ_TYPE_TREE: + err = diff_trees(obj_id1, obj_id2, path, diff_context, + 0, 0, dsa, repo, outfile); + break; + default: + err = got_error(GOT_ERR_OBJ_TYPE); + break; + } + free(obj_id1); + free(obj_id2); + } else { + obj_id2 = got_object_commit_get_tree_id(commit); + if (pcommit) + obj_id1 = got_object_commit_get_tree_id(pcommit); + fprintf(outfile, + "diff %s %s\n", id_str1 ? id_str1 : "/dev/null", id_str2); + fprintf(outfile, "commit - %s\n", + id_str1 ? id_str1 : "/dev/null"); + fprintf(outfile, "commit + %s\n", id_str2); + err = diff_trees(obj_id1, obj_id2, "", diff_context, 0, 0, + dsa, repo, outfile); + } +done: + free(id_str1); + free(id_str2); + if (pcommit) + got_object_commit_close(pcommit); + return err; +} + +static char * +get_datestr(time_t *time, char *datebuf) +{ + struct tm mytm, *tm; + char *p, *s; + + tm = gmtime_r(time, &mytm); + if (tm == NULL) + return NULL; + s = asctime_r(tm, datebuf); + if (s == NULL) + return NULL; + p = strchr(s, '\n'); + if (p) + *p = '\0'; + return s; +} + +static const struct got_error * +match_commit(int *have_match, struct got_object_id *id, + struct got_commit_object *commit, regex_t *regex) +{ + const struct got_error *err = NULL; + regmatch_t regmatch; + char *id_str = NULL, *logmsg = NULL; + + *have_match = 0; + + err = got_object_id_str(&id_str, id); + if (err) + return err; + + err = got_object_commit_get_logmsg(&logmsg, commit); + if (err) + goto done; + + if (regexec(regex, got_object_commit_get_author(commit), 1, + ®match, 0) == 0 || + regexec(regex, got_object_commit_get_committer(commit), 1, + ®match, 0) == 0 || + regexec(regex, id_str, 1, ®match, 0) == 0 || + regexec(regex, logmsg, 1, ®match, 0) == 0) + *have_match = 1; +done: + free(id_str); + free(logmsg); + return err; +} + +static void +match_changed_paths(int *have_match, struct got_pathlist_head *changed_paths, + regex_t *regex) +{ + regmatch_t regmatch; + struct got_pathlist_entry *pe; + + *have_match = 0; + + TAILQ_FOREACH(pe, changed_paths, entry) { + if (regexec(regex, pe->path, 1, ®match, 0) == 0) { + *have_match = 1; + break; + } + } +} + +static const struct got_error * +match_patch(int *have_match, struct got_commit_object *commit, + struct got_object_id *id, const char *path, int diff_context, + struct got_repository *repo, regex_t *regex, FILE *f) +{ + const struct got_error *err = NULL; + char *line = NULL; + size_t linesize = 0; + regmatch_t regmatch; + + *have_match = 0; + + err = got_opentemp_truncate(f); + if (err) + return err; + + err = print_patch(commit, id, path, diff_context, NULL, repo, f); + if (err) + goto done; + + if (fseeko(f, 0L, SEEK_SET) == -1) { + err = got_error_from_errno("fseeko"); + goto done; + } + + while (getline(&line, &linesize, f) != -1) { + if (regexec(regex, line, 1, ®match, 0) == 0) { + *have_match = 1; + break; + } + } +done: + free(line); + return err; +} + +#define GOT_COMMIT_SEP_STR "-----------------------------------------------\n" + +static const struct got_error* +build_refs_str(char **refs_str, struct got_reflist_head *refs, + struct got_object_id *id, struct got_repository *repo, + int local_only) +{ + static const struct got_error *err = NULL; + struct got_reflist_entry *re; + char *s; + const char *name; + + *refs_str = NULL; + + TAILQ_FOREACH(re, refs, entry) { + struct got_tag_object *tag = NULL; + struct got_object_id *ref_id; + int cmp; + + name = got_ref_get_name(re->ref); + if (strcmp(name, GOT_REF_HEAD) == 0) + continue; + if (strncmp(name, "refs/", 5) == 0) + name += 5; + if (strncmp(name, "got/", 4) == 0) + continue; + if (strncmp(name, "heads/", 6) == 0) + name += 6; + if (strncmp(name, "remotes/", 8) == 0) { + if (local_only) + continue; + name += 8; + s = strstr(name, "/" GOT_REF_HEAD); + if (s != NULL && strcmp(s, "/" GOT_REF_HEAD) == 0) + continue; + } + err = got_ref_resolve(&ref_id, repo, re->ref); + if (err) + break; + if (strncmp(name, "tags/", 5) == 0) { + err = got_object_open_as_tag(&tag, repo, ref_id); + if (err) { + if (err->code != GOT_ERR_OBJ_TYPE) { + free(ref_id); + break; + } + /* Ref points at something other than a tag. */ + err = NULL; + tag = NULL; + } + } + cmp = got_object_id_cmp(tag ? + got_object_tag_get_object_id(tag) : ref_id, id); + free(ref_id); + if (tag) + got_object_tag_close(tag); + if (cmp != 0) + continue; + s = *refs_str; + if (asprintf(refs_str, "%s%s%s", s ? s : "", + s ? ", " : "", name) == -1) { + err = got_error_from_errno("asprintf"); + free(s); + *refs_str = NULL; + break; + } + free(s); + } + + return err; +} + +static const struct got_error * +print_commit_oneline(struct got_commit_object *commit, struct got_object_id *id, + struct got_repository *repo, struct got_reflist_object_id_map *refs_idmap) +{ + const struct got_error *err = NULL; + char *ref_str = NULL, *id_str = NULL, *logmsg0 = NULL; + char *comma, *s, *nl; + struct got_reflist_head *refs; + char datebuf[12]; /* YYYY-MM-DD + SPACE + NUL */ + struct tm tm; + time_t committer_time; + + refs = got_reflist_object_id_map_lookup(refs_idmap, id); + if (refs) { + err = build_refs_str(&ref_str, refs, id, repo, 1); + if (err) + return err; + + /* Display the first matching ref only. */ + if (ref_str && (comma = strchr(ref_str, ',')) != NULL) + *comma = '\0'; + } + + if (ref_str == NULL) { + err = got_object_id_str(&id_str, id); + if (err) + return err; + } + + committer_time = got_object_commit_get_committer_time(commit); + if (gmtime_r(&committer_time, &tm) == NULL) { + err = got_error_from_errno("gmtime_r"); + goto done; + } + if (strftime(datebuf, sizeof(datebuf), "%G-%m-%d ", &tm) == 0) { + err = got_error(GOT_ERR_NO_SPACE); + goto done; + } + + err = got_object_commit_get_logmsg(&logmsg0, commit); + if (err) + goto done; + + s = logmsg0; + while (isspace((unsigned char)s[0])) + s++; + + nl = strchr(s, '\n'); + if (nl) { + *nl = '\0'; + } + + if (ref_str) + printf("%s%-7s %s\n", datebuf, ref_str, s); + else + printf("%s%.7s %s\n", datebuf, id_str, s); + + if (fflush(stdout) != 0 && err == NULL) + err = got_error_from_errno("fflush"); +done: + free(id_str); + free(ref_str); + free(logmsg0); + return err; +} + +static const struct got_error * +print_diffstat(struct got_diffstat_cb_arg *dsa, const char *header) +{ + struct got_pathlist_entry *pe; + + if (header != NULL) + printf("%s\n", header); + + TAILQ_FOREACH(pe, dsa->paths, entry) { + struct got_diff_changed_path *cp = pe->data; + int pad = dsa->max_path_len - pe->path_len + 1; + + printf(" %c %s%*c | %*d+ %*d-\n", cp->status, pe->path, pad, + ' ', dsa->add_cols + 1, cp->add, dsa->rm_cols + 1, cp->rm); + } + printf("\n%d file%s changed, %d insertion%s(+), %d deletion%s(-)\n\n", + dsa->nfiles, dsa->nfiles > 1 ? "s" : "", dsa->ins, + dsa->ins != 1 ? "s" : "", dsa->del, dsa->del != 1 ? "s" : ""); + + if (fflush(stdout) != 0) + return got_error_from_errno("fflush"); + + return NULL; +} + +static const struct got_error * +printfile(FILE *f) +{ + char buf[8192]; + size_t r; + + if (fseeko(f, 0L, SEEK_SET) == -1) + return got_error_from_errno("fseek"); + + for (;;) { + r = fread(buf, 1, sizeof(buf), f); + if (r == 0) { + if (ferror(f)) + return got_error_from_errno("fread"); + if (feof(f)) + break; + } + if (fwrite(buf, 1, r, stdout) != r) + return got_ferror(stdout, GOT_ERR_IO); + } + + return NULL; +} + +static const struct got_error * +print_commit(struct got_commit_object *commit, struct got_object_id *id, + struct got_repository *repo, const char *path, + struct got_pathlist_head *changed_paths, + struct got_diffstat_cb_arg *diffstat, int show_patch, int diff_context, + struct got_reflist_object_id_map *refs_idmap, const char *custom_refs_str, + const char *prefix) +{ + const struct got_error *err = NULL; + FILE *f = NULL; + char *id_str, *datestr, *logmsg0, *logmsg, *line; + char datebuf[26]; + time_t committer_time; + const char *author, *committer; + char *refs_str = NULL; + + err = got_object_id_str(&id_str, id); + if (err) + return err; + + if (custom_refs_str == NULL) { + struct got_reflist_head *refs; + refs = got_reflist_object_id_map_lookup(refs_idmap, id); + if (refs) { + err = build_refs_str(&refs_str, refs, id, repo, 0); + if (err) + goto done; + } + } + + printf(GOT_COMMIT_SEP_STR); + if (custom_refs_str) + printf("%s %s (%s)\n", prefix ? prefix : "commit", id_str, + custom_refs_str); + else + printf("%s %s%s%s%s\n", prefix ? prefix : "commit", id_str, + refs_str ? " (" : "", refs_str ? refs_str : "", + refs_str ? ")" : ""); + free(id_str); + id_str = NULL; + free(refs_str); + refs_str = NULL; + printf("from: %s\n", got_object_commit_get_author(commit)); + author = got_object_commit_get_author(commit); + committer = got_object_commit_get_committer(commit); + if (strcmp(author, committer) != 0) + printf("via: %s\n", committer); + committer_time = got_object_commit_get_committer_time(commit); + datestr = get_datestr(&committer_time, datebuf); + if (datestr) + printf("date: %s UTC\n", datestr); + if (got_object_commit_get_nparents(commit) > 1) { + const struct got_object_id_queue *parent_ids; + struct got_object_qid *qid; + int n = 1; + parent_ids = got_object_commit_get_parent_ids(commit); + STAILQ_FOREACH(qid, parent_ids, entry) { + err = got_object_id_str(&id_str, &qid->id); + if (err) + goto done; + printf("parent %d: %s\n", n++, id_str); + free(id_str); + id_str = NULL; + } + } + + err = got_object_commit_get_logmsg(&logmsg0, commit); + if (err) + goto done; + + logmsg = logmsg0; + do { + line = strsep(&logmsg, "\n"); + if (line) + printf(" %s\n", line); + } while (line); + free(logmsg0); + + if (changed_paths && diffstat == NULL) { + struct got_pathlist_entry *pe; + + TAILQ_FOREACH(pe, changed_paths, entry) { + struct got_diff_changed_path *cp = pe->data; + + printf(" %c %s\n", cp->status, pe->path); + } + printf("\n"); + } + if (show_patch) { + if (diffstat) { + f = got_opentemp(); + if (f == NULL) { + err = got_error_from_errno("got_opentemp"); + goto done; + } + } + + err = print_patch(commit, id, path, diff_context, diffstat, + repo, diffstat == NULL ? stdout : f); + if (err) + goto done; + } + if (diffstat) { + err = print_diffstat(diffstat, NULL); + if (err) + goto done; + if (show_patch) { + err = printfile(f); + if (err) + goto done; + } + } + if (show_patch) + printf("\n"); + + if (fflush(stdout) != 0 && err == NULL) + err = got_error_from_errno("fflush"); +done: + if (f && fclose(f) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + free(id_str); + free(refs_str); + return err; +} + +static const struct got_error * +print_commits(struct got_object_id *root_id, struct got_object_id *end_id, + struct got_repository *repo, const char *path, int show_changed_paths, + int show_diffstat, int show_patch, const char *search_pattern, + int diff_context, int limit, int log_branches, int reverse_display_order, + struct got_reflist_object_id_map *refs_idmap, int one_line, + FILE *tmpfile) +{ + const struct got_error *err; + struct got_commit_graph *graph; + regex_t regex; + int have_match; + struct got_object_id_queue reversed_commits; + struct got_object_qid *qid; + struct got_commit_object *commit; + struct got_pathlist_head changed_paths; + + STAILQ_INIT(&reversed_commits); + TAILQ_INIT(&changed_paths); + + if (search_pattern && regcomp(®ex, search_pattern, + REG_EXTENDED | REG_NOSUB | REG_NEWLINE)) + return got_error_msg(GOT_ERR_REGEX, search_pattern); + + err = got_commit_graph_open(&graph, path, !log_branches); + if (err) + return err; + err = got_commit_graph_iter_start(graph, root_id, repo, + check_cancelled, NULL); + if (err) + goto done; + for (;;) { + struct got_object_id id; + struct got_diffstat_cb_arg dsa = { 0, 0, 0, 0, 0, 0, + &changed_paths, 0, 0, GOT_DIFF_ALGORITHM_PATIENCE }; + + if (sigint_received || sigpipe_received) + break; + + err = got_commit_graph_iter_next(&id, graph, repo, + check_cancelled, NULL); + if (err) { + if (err->code == GOT_ERR_ITER_COMPLETED) + err = NULL; + break; + } + + err = got_object_open_as_commit(&commit, repo, &id); + if (err) + break; + + if ((show_changed_paths || (show_diffstat && !show_patch)) + && !reverse_display_order) { + err = get_changed_paths(&changed_paths, commit, repo, + show_diffstat ? &dsa : NULL); + if (err) + break; + } + + if (search_pattern) { + err = match_commit(&have_match, &id, commit, ®ex); + if (err) { + got_object_commit_close(commit); + break; + } + if (have_match == 0 && show_changed_paths) + match_changed_paths(&have_match, + &changed_paths, ®ex); + if (have_match == 0 && show_patch) { + err = match_patch(&have_match, commit, &id, + path, diff_context, repo, ®ex, tmpfile); + if (err) + break; + } + if (have_match == 0) { + got_object_commit_close(commit); + got_pathlist_free(&changed_paths, + GOT_PATHLIST_FREE_ALL); + continue; + } + } + + if (reverse_display_order) { + err = got_object_qid_alloc(&qid, &id); + if (err) + break; + STAILQ_INSERT_HEAD(&reversed_commits, qid, entry); + got_object_commit_close(commit); + } else { + if (one_line) + err = print_commit_oneline(commit, &id, + repo, refs_idmap); + else + err = print_commit(commit, &id, repo, path, + (show_changed_paths || show_diffstat) ? + &changed_paths : NULL, + show_diffstat ? &dsa : NULL, show_patch, + diff_context, refs_idmap, NULL, NULL); + got_object_commit_close(commit); + if (err) + break; + } + if ((limit && --limit == 0) || + (end_id && got_object_id_cmp(&id, end_id) == 0)) + break; + + got_pathlist_free(&changed_paths, GOT_PATHLIST_FREE_ALL); + } + if (reverse_display_order) { + STAILQ_FOREACH(qid, &reversed_commits, entry) { + struct got_diffstat_cb_arg dsa = { 0, 0, 0, 0, 0, 0, + &changed_paths, 0, 0, GOT_DIFF_ALGORITHM_PATIENCE }; + + err = got_object_open_as_commit(&commit, repo, + &qid->id); + if (err) + break; + if (show_changed_paths || + (show_diffstat && !show_patch)) { + err = get_changed_paths(&changed_paths, commit, + repo, show_diffstat ? &dsa : NULL); + if (err) + break; + } + if (one_line) + err = print_commit_oneline(commit, &qid->id, + repo, refs_idmap); + else + err = print_commit(commit, &qid->id, repo, path, + (show_changed_paths || show_diffstat) ? + &changed_paths : NULL, + show_diffstat ? &dsa : NULL, show_patch, + diff_context, refs_idmap, NULL, NULL); + got_object_commit_close(commit); + if (err) + break; + got_pathlist_free(&changed_paths, GOT_PATHLIST_FREE_ALL); + } + } +done: + while (!STAILQ_EMPTY(&reversed_commits)) { + qid = STAILQ_FIRST(&reversed_commits); + STAILQ_REMOVE_HEAD(&reversed_commits, entry); + got_object_qid_free(qid); + } + got_pathlist_free(&changed_paths, GOT_PATHLIST_FREE_ALL); + if (search_pattern) + regfree(®ex); + got_commit_graph_close(graph); + return err; +} + +__dead static void +usage_log(void) +{ + fprintf(stderr, "usage: %s log [-bdPpRs] [-C number] [-c commit] " + "[-l N] [-r repository-path] [-S search-pattern] [-x commit] " + "[path]\n", getprogname()); + exit(1); +} + +static int +get_default_log_limit(void) +{ + const char *got_default_log_limit; + long long n; + const char *errstr; + + got_default_log_limit = getenv("GOT_LOG_DEFAULT_LIMIT"); + if (got_default_log_limit == NULL) + return 0; + n = strtonum(got_default_log_limit, 0, INT_MAX, &errstr); + if (errstr != NULL) + return 0; + return n; +} + +static const struct got_error * +cmd_log(int argc, char *argv[]) +{ + const struct got_error *error; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + struct got_object_id *start_id = NULL, *end_id = NULL; + char *repo_path = NULL, *path = NULL, *cwd = NULL, *in_repo_path = NULL; + const char *start_commit = NULL, *end_commit = NULL; + const char *search_pattern = NULL; + int diff_context = -1, ch; + int show_changed_paths = 0, show_patch = 0, limit = 0, log_branches = 0; + int show_diffstat = 0, reverse_display_order = 0, one_line = 0; + const char *errstr; + struct got_reflist_head refs; + struct got_reflist_object_id_map *refs_idmap = NULL; + FILE *tmpfile = NULL; + int *pack_fds = NULL; + + TAILQ_INIT(&refs); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath flock proc exec sendfd unveil", + NULL) + == -1) + err(1, "pledge"); +#endif + + limit = get_default_log_limit(); + + while ((ch = getopt(argc, argv, "bC:c:dl:PpRr:S:sx:")) != -1) { + switch (ch) { + case 'b': + log_branches = 1; + break; + case 'C': + diff_context = strtonum(optarg, 0, GOT_DIFF_MAX_CONTEXT, + &errstr); + if (errstr != NULL) + errx(1, "number of context lines is %s: %s", + errstr, optarg); + break; + case 'c': + start_commit = optarg; + break; + case 'd': + show_diffstat = 1; + break; + case 'l': + limit = strtonum(optarg, 0, INT_MAX, &errstr); + if (errstr != NULL) + errx(1, "number of commits is %s: %s", + errstr, optarg); + break; + case 'P': + show_changed_paths = 1; + break; + case 'p': + show_patch = 1; + break; + case 'R': + reverse_display_order = 1; + break; + case 'r': + repo_path = realpath(optarg, NULL); + if (repo_path == NULL) + return got_error_from_errno2("realpath", + optarg); + got_path_strip_trailing_slashes(repo_path); + break; + case 'S': + search_pattern = optarg; + break; + case 's': + one_line = 1; + break; + case 'x': + end_commit = optarg; + break; + default: + usage_log(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (diff_context == -1) + diff_context = 3; + else if (!show_patch) + errx(1, "-C requires -p"); + + if (one_line && (show_patch || show_changed_paths || show_diffstat)) + errx(1, "cannot use -s with -d, -p or -P"); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + if (repo_path == NULL) { + error = got_worktree_open(&worktree, cwd); + if (error && error->code != GOT_ERR_NOT_WORKTREE) + goto done; + error = NULL; + } + + if (argc == 1) { + if (worktree) { + error = got_worktree_resolve_path(&path, worktree, + argv[0]); + if (error) + goto done; + } else { + path = strdup(argv[0]); + if (path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + } else if (argc != 0) + usage_log(); + + if (repo_path == NULL) { + repo_path = worktree ? + strdup(got_worktree_get_repo_path(worktree)) : strdup(cwd); + } + if (repo_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + + error = got_repo_open(&repo, repo_path, NULL, pack_fds); + if (error != NULL) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 1, + worktree ? got_worktree_get_root_path(worktree) : NULL); + if (error) + goto done; + + error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, NULL); + if (error) + goto done; + + error = got_reflist_object_id_map_create(&refs_idmap, &refs, repo); + if (error) + goto done; + + if (start_commit == NULL) { + struct got_reference *head_ref; + struct got_commit_object *commit = NULL; + error = got_ref_open(&head_ref, repo, + worktree ? got_worktree_get_head_ref_name(worktree) + : GOT_REF_HEAD, 0); + if (error != NULL) + goto done; + error = got_ref_resolve(&start_id, repo, head_ref); + got_ref_close(head_ref); + if (error != NULL) + goto done; + error = got_object_open_as_commit(&commit, repo, + start_id); + if (error != NULL) + goto done; + got_object_commit_close(commit); + } else { + error = got_repo_match_object_id(&start_id, NULL, + start_commit, GOT_OBJ_TYPE_COMMIT, &refs, repo); + if (error != NULL) + goto done; + } + if (end_commit != NULL) { + error = got_repo_match_object_id(&end_id, NULL, + end_commit, GOT_OBJ_TYPE_COMMIT, &refs, repo); + if (error != NULL) + goto done; + } + + if (worktree) { + /* + * If a path was specified on the command line it was resolved + * to a path in the work tree above. Prepend the work tree's + * path prefix to obtain the corresponding in-repository path. + */ + if (path) { + const char *prefix; + prefix = got_worktree_get_path_prefix(worktree); + if (asprintf(&in_repo_path, "%s%s%s", prefix, + (path[0] != '\0') ? "/" : "", path) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + } + } else + error = got_repo_map_path(&in_repo_path, repo, + path ? path : ""); + if (error != NULL) + goto done; + if (in_repo_path) { + free(path); + path = in_repo_path; + } + + if (worktree) { + /* Release work tree lock. */ + got_worktree_close(worktree); + worktree = NULL; + } + + if (search_pattern && show_patch) { + tmpfile = got_opentemp(); + if (tmpfile == NULL) { + error = got_error_from_errno("got_opentemp"); + goto done; + } + } + + error = print_commits(start_id, end_id, repo, path ? path : "", + show_changed_paths, show_diffstat, show_patch, search_pattern, + diff_context, limit, log_branches, reverse_display_order, + refs_idmap, one_line, tmpfile); +done: + free(path); + free(repo_path); + free(cwd); + if (worktree) + got_worktree_close(worktree); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + if (refs_idmap) + got_reflist_object_id_map_free(refs_idmap); + if (tmpfile && fclose(tmpfile) == EOF && error == NULL) + error = got_error_from_errno("fclose"); + got_ref_list_free(&refs); + return error; +} + +__dead static void +usage_diff(void) +{ + fprintf(stderr, "usage: %s diff [-adPsw] [-C number] [-c commit] " + "[-r repository-path] [object1 object2 | path ...]\n", + getprogname()); + exit(1); +} + +struct print_diff_arg { + struct got_repository *repo; + struct got_worktree *worktree; + struct got_diffstat_cb_arg *diffstat; + int diff_context; + const char *id_str; + int header_shown; + int diff_staged; + enum got_diff_algorithm diff_algo; + int ignore_whitespace; + int force_text_diff; + FILE *f1; + FILE *f2; + FILE *outfile; +}; + +/* + * Create a file which contains the target path of a symlink so we can feed + * it as content to the diff engine. + */ +static const struct got_error * +get_symlink_target_file(int *fd, int dirfd, const char *de_name, + const char *abspath) +{ + const struct got_error *err = NULL; + char target_path[PATH_MAX]; + ssize_t target_len, outlen; + + *fd = -1; + + if (dirfd != -1) { + target_len = readlinkat(dirfd, de_name, target_path, PATH_MAX); + if (target_len == -1) + return got_error_from_errno2("readlinkat", abspath); + } else { + target_len = readlink(abspath, target_path, PATH_MAX); + if (target_len == -1) + return got_error_from_errno2("readlink", abspath); + } + + *fd = got_opentempfd(); + if (*fd == -1) + return got_error_from_errno("got_opentempfd"); + + outlen = write(*fd, target_path, target_len); + if (outlen == -1) { + err = got_error_from_errno("got_opentempfd"); + goto done; + } + + if (lseek(*fd, 0, SEEK_SET) == -1) { + err = got_error_from_errno2("lseek", abspath); + goto done; + } +done: + if (err) { + close(*fd); + *fd = -1; + } + return err; +} + +static const struct got_error * +print_diff(void *arg, unsigned char status, unsigned char staged_status, + const char *path, struct got_object_id *blob_id, + struct got_object_id *staged_blob_id, struct got_object_id *commit_id, + int dirfd, const char *de_name) +{ + struct print_diff_arg *a = arg; + const struct got_error *err = NULL; + struct got_blob_object *blob1 = NULL; + int fd = -1, fd1 = -1, fd2 = -1; + FILE *f2 = NULL; + char *abspath = NULL, *label1 = NULL; + struct stat sb; + off_t size1 = 0; + int f2_exists = 0; + + memset(&sb, 0, sizeof(sb)); + + if (a->diff_staged) { + if (staged_status != GOT_STATUS_MODIFY && + staged_status != GOT_STATUS_ADD && + staged_status != GOT_STATUS_DELETE) + return NULL; + } else { + if (staged_status == GOT_STATUS_DELETE) + return NULL; + if (status == GOT_STATUS_NONEXISTENT) + return got_error_set_errno(ENOENT, path); + if (status != GOT_STATUS_MODIFY && + status != GOT_STATUS_ADD && + status != GOT_STATUS_DELETE && + status != GOT_STATUS_CONFLICT) + return NULL; + } + + err = got_opentemp_truncate(a->f1); + if (err) + return got_error_from_errno("got_opentemp_truncate"); + err = got_opentemp_truncate(a->f2); + if (err) + return got_error_from_errno("got_opentemp_truncate"); + + if (!a->header_shown) { + if (fprintf(a->outfile, "diff %s%s\n", + a->diff_staged ? "-s " : "", + got_worktree_get_root_path(a->worktree)) < 0) { + err = got_error_from_errno("fprintf"); + goto done; + } + if (fprintf(a->outfile, "commit - %s\n", a->id_str) < 0) { + err = got_error_from_errno("fprintf"); + goto done; + } + if (fprintf(a->outfile, "path + %s%s\n", + got_worktree_get_root_path(a->worktree), + a->diff_staged ? " (staged changes)" : "") < 0) { + err = got_error_from_errno("fprintf"); + goto done; + } + a->header_shown = 1; + } + + if (a->diff_staged) { + const char *label1 = NULL, *label2 = NULL; + switch (staged_status) { + case GOT_STATUS_MODIFY: + label1 = path; + label2 = path; + break; + case GOT_STATUS_ADD: + label2 = path; + break; + case GOT_STATUS_DELETE: + label1 = path; + break; + default: + return got_error(GOT_ERR_FILE_STATUS); + } + fd1 = got_opentempfd(); + if (fd1 == -1) { + err = got_error_from_errno("got_opentempfd"); + goto done; + } + fd2 = got_opentempfd(); + if (fd2 == -1) { + err = got_error_from_errno("got_opentempfd"); + goto done; + } + err = got_diff_objects_as_blobs(NULL, NULL, a->f1, a->f2, + fd1, fd2, blob_id, staged_blob_id, label1, label2, + a->diff_algo, a->diff_context, a->ignore_whitespace, + a->force_text_diff, a->diffstat, a->repo, a->outfile); + goto done; + } + + fd1 = got_opentempfd(); + if (fd1 == -1) { + err = got_error_from_errno("got_opentempfd"); + goto done; + } + + if (staged_status == GOT_STATUS_ADD || + staged_status == GOT_STATUS_MODIFY) { + char *id_str; + err = got_object_open_as_blob(&blob1, a->repo, staged_blob_id, + 8192, fd1); + if (err) + goto done; + err = got_object_id_str(&id_str, staged_blob_id); + if (err) + goto done; + if (asprintf(&label1, "%s (staged)", id_str) == -1) { + err = got_error_from_errno("asprintf"); + free(id_str); + goto done; + } + free(id_str); + } else if (status != GOT_STATUS_ADD) { + err = got_object_open_as_blob(&blob1, a->repo, blob_id, 8192, + fd1); + if (err) + goto done; + } + + if (status != GOT_STATUS_DELETE) { + if (asprintf(&abspath, "%s/%s", + got_worktree_get_root_path(a->worktree), path) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + + if (dirfd != -1) { + fd = openat(dirfd, de_name, + O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd == -1) { + if (!got_err_open_nofollow_on_symlink()) { + err = got_error_from_errno2("openat", + abspath); + goto done; + } + err = get_symlink_target_file(&fd, dirfd, + de_name, abspath); + if (err) + goto done; + } + } else { + fd = open(abspath, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd == -1) { + if (!got_err_open_nofollow_on_symlink()) { + err = got_error_from_errno2("open", + abspath); + goto done; + } + err = get_symlink_target_file(&fd, dirfd, + de_name, abspath); + if (err) + goto done; + } + } + if (fstatat(fd, abspath, &sb, AT_SYMLINK_NOFOLLOW) == -1) { + err = got_error_from_errno2("fstatat", abspath); + goto done; + } + f2 = fdopen(fd, "r"); + if (f2 == NULL) { + err = got_error_from_errno2("fdopen", abspath); + goto done; + } + fd = -1; + f2_exists = 1; + } + + if (blob1) { + err = got_object_blob_dump_to_file(&size1, NULL, NULL, + a->f1, blob1); + if (err) + goto done; + } + + err = got_diff_blob_file(blob1, a->f1, size1, label1, f2 ? f2 : a->f2, + f2_exists, &sb, path, GOT_DIFF_ALGORITHM_PATIENCE, a->diff_context, + a->ignore_whitespace, a->force_text_diff, a->diffstat, a->outfile); +done: + if (fd1 != -1 && close(fd1) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (fd2 != -1 && close(fd2) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (blob1) + got_object_blob_close(blob1); + if (fd != -1 && close(fd) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (f2 && fclose(f2) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + free(abspath); + return err; +} + +static const struct got_error * +cmd_diff(int argc, char *argv[]) +{ + const struct got_error *error; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + char *cwd = NULL, *repo_path = NULL; + const char *commit_args[2] = { NULL, NULL }; + int ncommit_args = 0; + struct got_object_id *ids[2] = { NULL, NULL }; + char *labels[2] = { NULL, NULL }; + int type1 = GOT_OBJ_TYPE_ANY, type2 = GOT_OBJ_TYPE_ANY; + int diff_context = 3, diff_staged = 0, ignore_whitespace = 0, ch, i; + int force_text_diff = 0, force_path = 0, rflag = 0, show_diffstat = 0; + const char *errstr; + struct got_reflist_head refs; + struct got_pathlist_head diffstat_paths, paths; + FILE *f1 = NULL, *f2 = NULL, *outfile = NULL; + int fd1 = -1, fd2 = -1; + int *pack_fds = NULL; + struct got_diffstat_cb_arg dsa; + + memset(&dsa, 0, sizeof(dsa)); + + TAILQ_INIT(&refs); + TAILQ_INIT(&paths); + TAILQ_INIT(&diffstat_paths); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath flock proc exec sendfd unveil", + NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "aC:c:dPr:sw")) != -1) { + switch (ch) { + case 'a': + force_text_diff = 1; + break; + case 'C': + diff_context = strtonum(optarg, 0, GOT_DIFF_MAX_CONTEXT, + &errstr); + if (errstr != NULL) + errx(1, "number of context lines is %s: %s", + errstr, optarg); + break; + case 'c': + if (ncommit_args >= 2) + errx(1, "too many -c options used"); + commit_args[ncommit_args++] = optarg; + break; + case 'd': + show_diffstat = 1; + break; + case 'P': + force_path = 1; + break; + case 'r': + repo_path = realpath(optarg, NULL); + if (repo_path == NULL) + return got_error_from_errno2("realpath", + optarg); + got_path_strip_trailing_slashes(repo_path); + rflag = 1; + break; + case 's': + diff_staged = 1; + break; + case 'w': + ignore_whitespace = 1; + break; + default: + usage_diff(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + if (repo_path == NULL) { + error = got_worktree_open(&worktree, cwd); + if (error && error->code != GOT_ERR_NOT_WORKTREE) + goto done; + else + error = NULL; + if (worktree) { + repo_path = + strdup(got_worktree_get_repo_path(worktree)); + if (repo_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else { + repo_path = strdup(cwd); + if (repo_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + } + + error = got_repo_open(&repo, repo_path, NULL, pack_fds); + free(repo_path); + if (error != NULL) + goto done; + + if (show_diffstat) { + dsa.paths = &diffstat_paths; + dsa.force_text = force_text_diff; + dsa.ignore_ws = ignore_whitespace; + dsa.diff_algo = GOT_DIFF_ALGORITHM_PATIENCE; + } + + if (rflag || worktree == NULL || ncommit_args > 0) { + if (force_path) { + error = got_error_msg(GOT_ERR_NOT_IMPL, + "-P option can only be used when diffing " + "a work tree"); + goto done; + } + if (diff_staged) { + error = got_error_msg(GOT_ERR_NOT_IMPL, + "-s option can only be used when diffing " + "a work tree"); + goto done; + } + } + + error = apply_unveil(got_repo_get_path(repo), 1, + worktree ? got_worktree_get_root_path(worktree) : NULL); + if (error) + goto done; + + if ((!force_path && argc == 2) || ncommit_args > 0) { + int obj_type = (ncommit_args > 0 ? + GOT_OBJ_TYPE_COMMIT : GOT_OBJ_TYPE_ANY); + error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, + NULL); + if (error) + goto done; + for (i = 0; i < (ncommit_args > 0 ? ncommit_args : argc); i++) { + const char *arg; + if (ncommit_args > 0) + arg = commit_args[i]; + else + arg = argv[i]; + error = got_repo_match_object_id(&ids[i], &labels[i], + arg, obj_type, &refs, repo); + if (error) { + if (error->code != GOT_ERR_NOT_REF && + error->code != GOT_ERR_NO_OBJ) + goto done; + if (ncommit_args > 0) + goto done; + error = NULL; + break; + } + } + } + + f1 = got_opentemp(); + if (f1 == NULL) { + error = got_error_from_errno("got_opentemp"); + goto done; + } + + f2 = got_opentemp(); + if (f2 == NULL) { + error = got_error_from_errno("got_opentemp"); + goto done; + } + + outfile = got_opentemp(); + if (outfile == NULL) { + error = got_error_from_errno("got_opentemp"); + goto done; + } + + if (ncommit_args == 0 && (ids[0] == NULL || ids[1] == NULL)) { + struct print_diff_arg arg; + char *id_str; + + if (worktree == NULL) { + if (argc == 2 && ids[0] == NULL) { + error = got_error_path(argv[0], GOT_ERR_NO_OBJ); + goto done; + } else if (argc == 2 && ids[1] == NULL) { + error = got_error_path(argv[1], GOT_ERR_NO_OBJ); + goto done; + } else if (argc > 0) { + error = got_error_fmt(GOT_ERR_NOT_WORKTREE, + "%s", "specified paths cannot be resolved"); + goto done; + } else { + error = got_error(GOT_ERR_NOT_WORKTREE); + goto done; + } + } + + error = get_worktree_paths_from_argv(&paths, argc, argv, + worktree); + if (error) + goto done; + + error = got_object_id_str(&id_str, + got_worktree_get_base_commit_id(worktree)); + if (error) + goto done; + arg.repo = repo; + arg.worktree = worktree; + arg.diff_algo = GOT_DIFF_ALGORITHM_PATIENCE; + arg.diff_context = diff_context; + arg.id_str = id_str; + arg.header_shown = 0; + arg.diff_staged = diff_staged; + arg.ignore_whitespace = ignore_whitespace; + arg.force_text_diff = force_text_diff; + arg.diffstat = show_diffstat ? &dsa : NULL; + arg.f1 = f1; + arg.f2 = f2; + arg.outfile = outfile; + + error = got_worktree_status(worktree, &paths, repo, 0, + print_diff, &arg, check_cancelled, NULL); + free(id_str); + if (error) + goto done; + + if (show_diffstat && dsa.nfiles > 0) { + char *header; + + if (asprintf(&header, "diffstat %s%s", + diff_staged ? "-s " : "", + got_worktree_get_root_path(worktree)) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + + error = print_diffstat(&dsa, header); + free(header); + if (error) + goto done; + } + + error = printfile(outfile); + goto done; + } + + if (ncommit_args == 1) { + struct got_commit_object *commit; + error = got_object_open_as_commit(&commit, repo, ids[0]); + if (error) + goto done; + + labels[1] = labels[0]; + ids[1] = ids[0]; + if (got_object_commit_get_nparents(commit) > 0) { + const struct got_object_id_queue *pids; + struct got_object_qid *pid; + pids = got_object_commit_get_parent_ids(commit); + pid = STAILQ_FIRST(pids); + ids[0] = got_object_id_dup(&pid->id); + if (ids[0] == NULL) { + error = got_error_from_errno( + "got_object_id_dup"); + got_object_commit_close(commit); + goto done; + } + error = got_object_id_str(&labels[0], ids[0]); + if (error) { + got_object_commit_close(commit); + goto done; + } + } else { + ids[0] = NULL; + labels[0] = strdup("/dev/null"); + if (labels[0] == NULL) { + error = got_error_from_errno("strdup"); + got_object_commit_close(commit); + goto done; + } + } + + got_object_commit_close(commit); + } + + if (ncommit_args == 0 && argc > 2) { + error = got_error_msg(GOT_ERR_BAD_PATH, + "path arguments cannot be used when diffing two objects"); + goto done; + } + + if (ids[0]) { + error = got_object_get_type(&type1, repo, ids[0]); + if (error) + goto done; + } + + error = got_object_get_type(&type2, repo, ids[1]); + if (error) + goto done; + if (type1 != GOT_OBJ_TYPE_ANY && type1 != type2) { + error = got_error(GOT_ERR_OBJ_TYPE); + goto done; + } + if (type1 == GOT_OBJ_TYPE_BLOB && argc > 2) { + error = got_error_msg(GOT_ERR_OBJ_TYPE, + "path arguments cannot be used when diffing blobs"); + goto done; + } + + for (i = 0; ncommit_args > 0 && i < argc; i++) { + char *in_repo_path; + struct got_pathlist_entry *new; + if (worktree) { + const char *prefix; + char *p; + error = got_worktree_resolve_path(&p, worktree, + argv[i]); + if (error) + goto done; + prefix = got_worktree_get_path_prefix(worktree); + while (prefix[0] == '/') + prefix++; + if (asprintf(&in_repo_path, "%s%s%s", prefix, + (p[0] != '\0' && prefix[0] != '\0') ? "/" : "", + p) == -1) { + error = got_error_from_errno("asprintf"); + free(p); + goto done; + } + free(p); + } else { + char *mapped_path, *s; + error = got_repo_map_path(&mapped_path, repo, argv[i]); + if (error) + goto done; + s = mapped_path; + while (s[0] == '/') + s++; + in_repo_path = strdup(s); + if (in_repo_path == NULL) { + error = got_error_from_errno("asprintf"); + free(mapped_path); + goto done; + } + free(mapped_path); + + } + error = got_pathlist_insert(&new, &paths, in_repo_path, NULL); + if (error || new == NULL /* duplicate */) + free(in_repo_path); + if (error) + goto done; + } + + if (worktree) { + /* Release work tree lock. */ + got_worktree_close(worktree); + worktree = NULL; + } + + fd1 = got_opentempfd(); + if (fd1 == -1) { + error = got_error_from_errno("got_opentempfd"); + goto done; + } + + fd2 = got_opentempfd(); + if (fd2 == -1) { + error = got_error_from_errno("got_opentempfd"); + goto done; + } + + switch (type1 == GOT_OBJ_TYPE_ANY ? type2 : type1) { + case GOT_OBJ_TYPE_BLOB: + error = got_diff_objects_as_blobs(NULL, NULL, f1, f2, + fd1, fd2, ids[0], ids[1], NULL, NULL, + GOT_DIFF_ALGORITHM_PATIENCE, diff_context, + ignore_whitespace, force_text_diff, + show_diffstat ? &dsa : NULL, repo, outfile); + break; + case GOT_OBJ_TYPE_TREE: + error = got_diff_objects_as_trees(NULL, NULL, f1, f2, fd1, fd2, + ids[0], ids[1], &paths, "", "", + GOT_DIFF_ALGORITHM_PATIENCE, diff_context, + ignore_whitespace, force_text_diff, + show_diffstat ? &dsa : NULL, repo, outfile); + break; + case GOT_OBJ_TYPE_COMMIT: + fprintf(outfile, "diff %s %s\n", labels[0], labels[1]); + error = got_diff_objects_as_commits(NULL, NULL, f1, f2, + fd1, fd2, ids[0], ids[1], &paths, + GOT_DIFF_ALGORITHM_PATIENCE, diff_context, + ignore_whitespace, force_text_diff, + show_diffstat ? &dsa : NULL, repo, outfile); + break; + default: + error = got_error(GOT_ERR_OBJ_TYPE); + } + if (error) + goto done; + + if (show_diffstat && dsa.nfiles > 0) { + char *header = NULL; + + if (asprintf(&header, "diffstat %s %s", + labels[0], labels[1]) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + + error = print_diffstat(&dsa, header); + free(header); + if (error) + goto done; + } + + error = printfile(outfile); + +done: + free(labels[0]); + free(labels[1]); + free(ids[0]); + free(ids[1]); + if (worktree) + got_worktree_close(worktree); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + got_pathlist_free(&paths, GOT_PATHLIST_FREE_PATH); + got_pathlist_free(&diffstat_paths, GOT_PATHLIST_FREE_ALL); + got_ref_list_free(&refs); + if (outfile && fclose(outfile) == EOF && error == NULL) + error = got_error_from_errno("fclose"); + if (f1 && fclose(f1) == EOF && error == NULL) + error = got_error_from_errno("fclose"); + if (f2 && fclose(f2) == EOF && error == NULL) + error = got_error_from_errno("fclose"); + if (fd1 != -1 && close(fd1) == -1 && error == NULL) + error = got_error_from_errno("close"); + if (fd2 != -1 && close(fd2) == -1 && error == NULL) + error = got_error_from_errno("close"); + return error; +} + +__dead static void +usage_blame(void) +{ + fprintf(stderr, + "usage: %s blame [-c commit] [-r repository-path] path\n", + getprogname()); + exit(1); +} + +struct blame_line { + int annotated; + char *id_str; + char *committer; + char datebuf[11]; /* YYYY-MM-DD + NUL */ +}; + +struct blame_cb_args { + struct blame_line *lines; + int nlines; + int nlines_prec; + int lineno_cur; + off_t *line_offsets; + FILE *f; + struct got_repository *repo; +}; + +static const struct got_error * +blame_cb(void *arg, int nlines, int lineno, + struct got_commit_object *commit, struct got_object_id *id) +{ + const struct got_error *err = NULL; + struct blame_cb_args *a = arg; + struct blame_line *bline; + char *line = NULL; + size_t linesize = 0; + off_t offset; + struct tm tm; + time_t committer_time; + + if (nlines != a->nlines || + (lineno != -1 && lineno < 1) || lineno > a->nlines) + return got_error(GOT_ERR_RANGE); + + if (sigint_received) + return got_error(GOT_ERR_ITER_COMPLETED); + + if (lineno == -1) + return NULL; /* no change in this commit */ + + /* Annotate this line. */ + bline = &a->lines[lineno - 1]; + if (bline->annotated) + return NULL; + err = got_object_id_str(&bline->id_str, id); + if (err) + return err; + + bline->committer = strdup(got_object_commit_get_committer(commit)); + if (bline->committer == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + + committer_time = got_object_commit_get_committer_time(commit); + if (gmtime_r(&committer_time, &tm) == NULL) + return got_error_from_errno("gmtime_r"); + if (strftime(bline->datebuf, sizeof(bline->datebuf), "%G-%m-%d", + &tm) == 0) { + err = got_error(GOT_ERR_NO_SPACE); + goto done; + } + bline->annotated = 1; + + /* Print lines annotated so far. */ + bline = &a->lines[a->lineno_cur - 1]; + if (!bline->annotated) + goto done; + + offset = a->line_offsets[a->lineno_cur - 1]; + if (fseeko(a->f, offset, SEEK_SET) == -1) { + err = got_error_from_errno("fseeko"); + goto done; + } + + while (a->lineno_cur <= a->nlines && bline->annotated) { + char *smallerthan, *at, *nl, *committer; + size_t len; + + if (getline(&line, &linesize, a->f) == -1) { + if (ferror(a->f)) + err = got_error_from_errno("getline"); + break; + } + + committer = bline->committer; + smallerthan = strchr(committer, '<'); + if (smallerthan && smallerthan[1] != '\0') + committer = smallerthan + 1; + at = strchr(committer, '@'); + if (at) + *at = '\0'; + len = strlen(committer); + if (len >= 9) + committer[8] = '\0'; + + nl = strchr(line, '\n'); + if (nl) + *nl = '\0'; + printf("%.*d) %.8s %s %-8s %s\n", a->nlines_prec, a->lineno_cur, + bline->id_str, bline->datebuf, committer, line); + + a->lineno_cur++; + bline = &a->lines[a->lineno_cur - 1]; + } +done: + free(line); + return err; +} + +static const struct got_error * +cmd_blame(int argc, char *argv[]) +{ + const struct got_error *error; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + char *path, *cwd = NULL, *repo_path = NULL, *in_repo_path = NULL; + char *link_target = NULL; + struct got_object_id *obj_id = NULL; + struct got_object_id *commit_id = NULL; + struct got_commit_object *commit = NULL; + struct got_blob_object *blob = NULL; + char *commit_id_str = NULL; + struct blame_cb_args bca; + int ch, obj_type, i, fd1 = -1, fd2 = -1, fd3 = -1; + off_t filesize; + int *pack_fds = NULL; + FILE *f1 = NULL, *f2 = NULL; + + fd1 = got_opentempfd(); + if (fd1 == -1) + return got_error_from_errno("got_opentempfd"); + + memset(&bca, 0, sizeof(bca)); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath flock proc exec sendfd unveil", + NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "c:r:")) != -1) { + switch (ch) { + case 'c': + commit_id_str = optarg; + break; + case 'r': + repo_path = realpath(optarg, NULL); + if (repo_path == NULL) + return got_error_from_errno2("realpath", + optarg); + got_path_strip_trailing_slashes(repo_path); + break; + default: + usage_blame(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (argc == 1) + path = argv[0]; + else + usage_blame(); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + if (repo_path == NULL) { + error = got_worktree_open(&worktree, cwd); + if (error && error->code != GOT_ERR_NOT_WORKTREE) + goto done; + else + error = NULL; + if (worktree) { + repo_path = + strdup(got_worktree_get_repo_path(worktree)); + if (repo_path == NULL) { + error = got_error_from_errno("strdup"); + if (error) + goto done; + } + } else { + repo_path = strdup(cwd); + if (repo_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + } + + error = got_repo_open(&repo, repo_path, NULL, pack_fds); + if (error != NULL) + goto done; + + if (worktree) { + const char *prefix = got_worktree_get_path_prefix(worktree); + char *p; + + error = got_worktree_resolve_path(&p, worktree, path); + if (error) + goto done; + if (asprintf(&in_repo_path, "%s%s%s", prefix, + (p[0] != '\0' && !got_path_is_root_dir(prefix)) ? "/" : "", + p) == -1) { + error = got_error_from_errno("asprintf"); + free(p); + goto done; + } + free(p); + error = apply_unveil(got_repo_get_path(repo), 1, NULL); + } else { + error = apply_unveil(got_repo_get_path(repo), 1, NULL); + if (error) + goto done; + error = got_repo_map_path(&in_repo_path, repo, path); + } + if (error) + goto done; + + if (commit_id_str == NULL) { + struct got_reference *head_ref; + error = got_ref_open(&head_ref, repo, worktree ? + got_worktree_get_head_ref_name(worktree) : GOT_REF_HEAD, 0); + if (error != NULL) + goto done; + error = got_ref_resolve(&commit_id, repo, head_ref); + got_ref_close(head_ref); + if (error != NULL) + goto done; + } else { + struct got_reflist_head refs; + TAILQ_INIT(&refs); + error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, + NULL); + if (error) + goto done; + error = got_repo_match_object_id(&commit_id, NULL, + commit_id_str, GOT_OBJ_TYPE_COMMIT, &refs, repo); + got_ref_list_free(&refs); + if (error) + goto done; + } + + if (worktree) { + /* Release work tree lock. */ + got_worktree_close(worktree); + worktree = NULL; + } + + error = got_object_open_as_commit(&commit, repo, commit_id); + if (error) + goto done; + + error = got_object_resolve_symlinks(&link_target, in_repo_path, + commit, repo); + if (error) + goto done; + + error = got_object_id_by_path(&obj_id, repo, commit, + link_target ? link_target : in_repo_path); + if (error) + goto done; + + error = got_object_get_type(&obj_type, repo, obj_id); + if (error) + goto done; + + if (obj_type != GOT_OBJ_TYPE_BLOB) { + error = got_error_path(link_target ? link_target : in_repo_path, + GOT_ERR_OBJ_TYPE); + goto done; + } + + error = got_object_open_as_blob(&blob, repo, obj_id, 8192, fd1); + if (error) + goto done; + bca.f = got_opentemp(); + if (bca.f == NULL) { + error = got_error_from_errno("got_opentemp"); + goto done; + } + error = got_object_blob_dump_to_file(&filesize, &bca.nlines, + &bca.line_offsets, bca.f, blob); + if (error || bca.nlines == 0) + goto done; + + /* Don't include \n at EOF in the blame line count. */ + if (bca.line_offsets[bca.nlines - 1] == filesize) + bca.nlines--; + + bca.lines = calloc(bca.nlines, sizeof(*bca.lines)); + if (bca.lines == NULL) { + error = got_error_from_errno("calloc"); + goto done; + } + bca.lineno_cur = 1; + bca.nlines_prec = 0; + i = bca.nlines; + while (i > 0) { + i /= 10; + bca.nlines_prec++; + } + bca.repo = repo; + + fd2 = got_opentempfd(); + if (fd2 == -1) { + error = got_error_from_errno("got_opentempfd"); + goto done; + } + fd3 = got_opentempfd(); + if (fd3 == -1) { + error = got_error_from_errno("got_opentempfd"); + goto done; + } + f1 = got_opentemp(); + if (f1 == NULL) { + error = got_error_from_errno("got_opentemp"); + goto done; + } + f2 = got_opentemp(); + if (f2 == NULL) { + error = got_error_from_errno("got_opentemp"); + goto done; + } + error = got_blame(link_target ? link_target : in_repo_path, commit_id, + repo, GOT_DIFF_ALGORITHM_PATIENCE, blame_cb, &bca, + check_cancelled, NULL, fd2, fd3, f1, f2); +done: + free(in_repo_path); + free(link_target); + free(repo_path); + free(cwd); + free(commit_id); + free(obj_id); + if (commit) + got_object_commit_close(commit); + + if (fd1 != -1 && close(fd1) == -1 && error == NULL) + error = got_error_from_errno("close"); + if (fd2 != -1 && close(fd2) == -1 && error == NULL) + error = got_error_from_errno("close"); + if (fd3 != -1 && close(fd3) == -1 && error == NULL) + error = got_error_from_errno("close"); + if (f1 && fclose(f1) == EOF && error == NULL) + error = got_error_from_errno("fclose"); + if (f2 && fclose(f2) == EOF && error == NULL) + error = got_error_from_errno("fclose"); + + if (blob) + got_object_blob_close(blob); + if (worktree) + got_worktree_close(worktree); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + if (bca.lines) { + for (i = 0; i < bca.nlines; i++) { + struct blame_line *bline = &bca.lines[i]; + free(bline->id_str); + free(bline->committer); + } + free(bca.lines); + } + free(bca.line_offsets); + if (bca.f && fclose(bca.f) == EOF && error == NULL) + error = got_error_from_errno("fclose"); + return error; +} + +__dead static void +usage_tree(void) +{ + fprintf(stderr, "usage: %s tree [-iR] [-c commit] [-r repository-path] " + "[path]\n", getprogname()); + exit(1); +} + +static const struct got_error * +print_entry(struct got_tree_entry *te, const char *id, const char *path, + const char *root_path, struct got_repository *repo) +{ + const struct got_error *err = NULL; + int is_root_path = (strcmp(path, root_path) == 0); + const char *modestr = ""; + mode_t mode = got_tree_entry_get_mode(te); + char *link_target = NULL; + + path += strlen(root_path); + while (path[0] == '/') + path++; + + if (got_object_tree_entry_is_submodule(te)) + modestr = "$"; + else if (S_ISLNK(mode)) { + int i; + + err = got_tree_entry_get_symlink_target(&link_target, te, repo); + if (err) + return err; + for (i = 0; link_target[i] != '\0'; i++) { + if (!isprint((unsigned char)link_target[i])) + link_target[i] = '?'; + } + + modestr = "@"; + } + else if (S_ISDIR(mode)) + modestr = "/"; + else if (mode & S_IXUSR) + modestr = "*"; + + printf("%s%s%s%s%s%s%s\n", id ? id : "", path, + is_root_path ? "" : "/", got_tree_entry_get_name(te), modestr, + link_target ? " -> ": "", link_target ? link_target : ""); + + free(link_target); + return NULL; +} + +static const struct got_error * +print_tree(const char *path, struct got_commit_object *commit, + int show_ids, int recurse, const char *root_path, + struct got_repository *repo) +{ + const struct got_error *err = NULL; + struct got_object_id *tree_id = NULL; + struct got_tree_object *tree = NULL; + int nentries, i; + + err = got_object_id_by_path(&tree_id, repo, commit, path); + if (err) + goto done; + + err = got_object_open_as_tree(&tree, repo, tree_id); + if (err) + goto done; + nentries = got_object_tree_get_nentries(tree); + for (i = 0; i < nentries; i++) { + struct got_tree_entry *te; + char *id = NULL; + + if (sigint_received || sigpipe_received) + break; + + te = got_object_tree_get_entry(tree, i); + if (show_ids) { + char *id_str; + err = got_object_id_str(&id_str, + got_tree_entry_get_id(te)); + if (err) + goto done; + if (asprintf(&id, "%s ", id_str) == -1) { + err = got_error_from_errno("asprintf"); + free(id_str); + goto done; + } + free(id_str); + } + err = print_entry(te, id, path, root_path, repo); + free(id); + if (err) + goto done; + + if (recurse && S_ISDIR(got_tree_entry_get_mode(te))) { + char *child_path; + if (asprintf(&child_path, "%s%s%s", path, + path[0] == '/' && path[1] == '\0' ? "" : "/", + got_tree_entry_get_name(te)) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + err = print_tree(child_path, commit, show_ids, 1, + root_path, repo); + free(child_path); + if (err) + goto done; + } + } +done: + if (tree) + got_object_tree_close(tree); + free(tree_id); + return err; +} + +static const struct got_error * +cmd_tree(int argc, char *argv[]) +{ + const struct got_error *error; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + const char *path, *refname = NULL; + char *cwd = NULL, *repo_path = NULL, *in_repo_path = NULL; + struct got_object_id *commit_id = NULL; + struct got_commit_object *commit = NULL; + char *commit_id_str = NULL; + int show_ids = 0, recurse = 0; + int ch; + int *pack_fds = NULL; + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath flock proc exec sendfd unveil", + NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "c:iRr:")) != -1) { + switch (ch) { + case 'c': + commit_id_str = optarg; + break; + case 'i': + show_ids = 1; + break; + case 'R': + recurse = 1; + break; + case 'r': + repo_path = realpath(optarg, NULL); + if (repo_path == NULL) + return got_error_from_errno2("realpath", + optarg); + got_path_strip_trailing_slashes(repo_path); + break; + default: + usage_tree(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (argc == 1) + path = argv[0]; + else if (argc > 1) + usage_tree(); + else + path = NULL; + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + if (repo_path == NULL) { + error = got_worktree_open(&worktree, cwd); + if (error && error->code != GOT_ERR_NOT_WORKTREE) + goto done; + else + error = NULL; + if (worktree) { + repo_path = + strdup(got_worktree_get_repo_path(worktree)); + if (repo_path == NULL) + error = got_error_from_errno("strdup"); + if (error) + goto done; + } else { + repo_path = strdup(cwd); + if (repo_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + } + + error = got_repo_open(&repo, repo_path, NULL, pack_fds); + if (error != NULL) + goto done; + + if (worktree) { + const char *prefix = got_worktree_get_path_prefix(worktree); + char *p; + + if (path == NULL || got_path_is_root_dir(path)) + path = ""; + error = got_worktree_resolve_path(&p, worktree, path); + if (error) + goto done; + if (asprintf(&in_repo_path, "%s%s%s", prefix, + (p[0] != '\0' && !got_path_is_root_dir(prefix)) ? "/" : "", + p) == -1) { + error = got_error_from_errno("asprintf"); + free(p); + goto done; + } + free(p); + error = apply_unveil(got_repo_get_path(repo), 1, NULL); + if (error) + goto done; + } else { + error = apply_unveil(got_repo_get_path(repo), 1, NULL); + if (error) + goto done; + if (path == NULL) + path = "/"; + error = got_repo_map_path(&in_repo_path, repo, path); + if (error != NULL) + goto done; + } + + if (commit_id_str == NULL) { + struct got_reference *head_ref; + if (worktree) + refname = got_worktree_get_head_ref_name(worktree); + else + refname = GOT_REF_HEAD; + error = got_ref_open(&head_ref, repo, refname, 0); + if (error != NULL) + goto done; + error = got_ref_resolve(&commit_id, repo, head_ref); + got_ref_close(head_ref); + if (error != NULL) + goto done; + } else { + struct got_reflist_head refs; + TAILQ_INIT(&refs); + error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, + NULL); + if (error) + goto done; + error = got_repo_match_object_id(&commit_id, NULL, + commit_id_str, GOT_OBJ_TYPE_COMMIT, &refs, repo); + got_ref_list_free(&refs); + if (error) + goto done; + } + + if (worktree) { + /* Release work tree lock. */ + got_worktree_close(worktree); + worktree = NULL; + } + + error = got_object_open_as_commit(&commit, repo, commit_id); + if (error) + goto done; + + error = print_tree(in_repo_path, commit, show_ids, recurse, + in_repo_path, repo); +done: + free(in_repo_path); + free(repo_path); + free(cwd); + free(commit_id); + if (commit) + got_object_commit_close(commit); + if (worktree) + got_worktree_close(worktree); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + return error; +} + +__dead static void +usage_status(void) +{ + fprintf(stderr, "usage: %s status [-I] [-S status-codes] " + "[-s status-codes] [path ...]\n", getprogname()); + exit(1); +} + +struct got_status_arg { + char *status_codes; + int suppress; +}; + +static const struct got_error * +print_status(void *arg, unsigned char status, unsigned char staged_status, + const char *path, struct got_object_id *blob_id, + struct got_object_id *staged_blob_id, struct got_object_id *commit_id, + int dirfd, const char *de_name) +{ + struct got_status_arg *st = arg; + + if (status == staged_status && (status == GOT_STATUS_DELETE)) + status = GOT_STATUS_NO_CHANGE; + if (st != NULL && st->status_codes) { + size_t ncodes = strlen(st->status_codes); + int i, j = 0; + + for (i = 0; i < ncodes ; i++) { + if (st->suppress) { + if (status == st->status_codes[i] || + staged_status == st->status_codes[i]) { + j++; + continue; + } + } else { + if (status == st->status_codes[i] || + staged_status == st->status_codes[i]) + break; + } + } + + if (st->suppress && j == 0) + goto print; + + if (i == ncodes) + return NULL; + } +print: + printf("%c%c %s\n", status, staged_status, path); + return NULL; +} + +static const struct got_error * +cmd_status(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + struct got_status_arg st; + char *cwd = NULL; + struct got_pathlist_head paths; + int ch, i, no_ignores = 0; + int *pack_fds = NULL; + + TAILQ_INIT(&paths); + + memset(&st, 0, sizeof(st)); + st.status_codes = NULL; + st.suppress = 0; + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath flock proc exec sendfd unveil", + NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "IS:s:")) != -1) { + switch (ch) { + case 'I': + no_ignores = 1; + break; + case 'S': + if (st.status_codes != NULL && st.suppress == 0) + option_conflict('S', 's'); + st.suppress = 1; + /* fallthrough */ + case 's': + for (i = 0; optarg[i] != '\0'; i++) { + switch (optarg[i]) { + case GOT_STATUS_MODIFY: + case GOT_STATUS_ADD: + case GOT_STATUS_DELETE: + case GOT_STATUS_CONFLICT: + case GOT_STATUS_MISSING: + case GOT_STATUS_OBSTRUCTED: + case GOT_STATUS_UNVERSIONED: + case GOT_STATUS_MODE_CHANGE: + case GOT_STATUS_NONEXISTENT: + break; + default: + errx(1, "invalid status code '%c'", + optarg[i]); + } + } + if (ch == 's' && st.suppress) + option_conflict('s', 'S'); + st.status_codes = optarg; + break; + default: + usage_status(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_open(&worktree, cwd); + if (error) { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, "status", cwd); + goto done; + } + + error = got_repo_open(&repo, got_worktree_get_repo_path(worktree), + NULL, pack_fds); + if (error != NULL) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 1, + got_worktree_get_root_path(worktree)); + if (error) + goto done; + + error = get_worktree_paths_from_argv(&paths, argc, argv, worktree); + if (error) + goto done; + + error = got_worktree_status(worktree, &paths, repo, no_ignores, + print_status, &st, check_cancelled, NULL); +done: + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + + got_pathlist_free(&paths, GOT_PATHLIST_FREE_PATH); + free(cwd); + return error; +} + +__dead static void +usage_ref(void) +{ + fprintf(stderr, "usage: %s ref [-dlt] [-c object] [-r repository-path] " + "[-s reference] [name]\n", getprogname()); + exit(1); +} + +static const struct got_error * +list_refs(struct got_repository *repo, const char *refname, int sort_by_time) +{ + static const struct got_error *err = NULL; + struct got_reflist_head refs; + struct got_reflist_entry *re; + + TAILQ_INIT(&refs); + err = got_ref_list(&refs, repo, refname, sort_by_time ? + got_ref_cmp_by_commit_timestamp_descending : got_ref_cmp_by_name, + repo); + if (err) + return err; + + TAILQ_FOREACH(re, &refs, entry) { + char *refstr; + refstr = got_ref_to_str(re->ref); + if (refstr == NULL) { + err = got_error_from_errno("got_ref_to_str"); + break; + } + printf("%s: %s\n", got_ref_get_name(re->ref), refstr); + free(refstr); + } + + got_ref_list_free(&refs); + return err; +} + +static const struct got_error * +delete_ref_by_name(struct got_repository *repo, const char *refname) +{ + const struct got_error *err; + struct got_reference *ref; + + err = got_ref_open(&ref, repo, refname, 0); + if (err) + return err; + + err = delete_ref(repo, ref); + got_ref_close(ref); + return err; +} + +static const struct got_error * +add_ref(struct got_repository *repo, const char *refname, const char *target) +{ + const struct got_error *err = NULL; + struct got_object_id *id = NULL; + struct got_reference *ref = NULL; + struct got_reflist_head refs; + + /* + * Don't let the user create a reference name with a leading '-'. + * While technically a valid reference name, this case is usually + * an unintended typo. + */ + if (refname[0] == '-') + return got_error_path(refname, GOT_ERR_REF_NAME_MINUS); + + TAILQ_INIT(&refs); + err = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, NULL); + if (err) + goto done; + err = got_repo_match_object_id(&id, NULL, target, GOT_OBJ_TYPE_ANY, + &refs, repo); + got_ref_list_free(&refs); + if (err) + goto done; + + err = got_ref_alloc(&ref, refname, id); + if (err) + goto done; + + err = got_ref_write(ref, repo); +done: + if (ref) + got_ref_close(ref); + free(id); + return err; +} + +static const struct got_error * +add_symref(struct got_repository *repo, const char *refname, const char *target) +{ + const struct got_error *err = NULL; + struct got_reference *ref = NULL; + struct got_reference *target_ref = NULL; + + /* + * Don't let the user create a reference name with a leading '-'. + * While technically a valid reference name, this case is usually + * an unintended typo. + */ + if (refname[0] == '-') + return got_error_path(refname, GOT_ERR_REF_NAME_MINUS); + + err = got_ref_open(&target_ref, repo, target, 0); + if (err) + return err; + + err = got_ref_alloc_symref(&ref, refname, target_ref); + if (err) + goto done; + + err = got_ref_write(ref, repo); +done: + if (target_ref) + got_ref_close(target_ref); + if (ref) + got_ref_close(ref); + return err; +} + +static const struct got_error * +cmd_ref(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + char *cwd = NULL, *repo_path = NULL; + int ch, do_list = 0, do_delete = 0, sort_by_time = 0; + const char *obj_arg = NULL, *symref_target= NULL; + char *refname = NULL; + int *pack_fds = NULL; + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec " + "sendfd unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "c:dlr:s:t")) != -1) { + switch (ch) { + case 'c': + obj_arg = optarg; + break; + case 'd': + do_delete = 1; + break; + case 'l': + do_list = 1; + break; + case 'r': + repo_path = realpath(optarg, NULL); + if (repo_path == NULL) + return got_error_from_errno2("realpath", + optarg); + got_path_strip_trailing_slashes(repo_path); + break; + case 's': + symref_target = optarg; + break; + case 't': + sort_by_time = 1; + break; + default: + usage_ref(); + /* NOTREACHED */ + } + } + + if (obj_arg && do_list) + option_conflict('c', 'l'); + if (obj_arg && do_delete) + option_conflict('c', 'd'); + if (obj_arg && symref_target) + option_conflict('c', 's'); + if (symref_target && do_delete) + option_conflict('s', 'd'); + if (symref_target && do_list) + option_conflict('s', 'l'); + if (do_delete && do_list) + option_conflict('d', 'l'); + if (sort_by_time && !do_list) + errx(1, "-t option requires -l option"); + + argc -= optind; + argv += optind; + + if (do_list) { + if (argc != 0 && argc != 1) + usage_ref(); + if (argc == 1) { + refname = strdup(argv[0]); + if (refname == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + } else { + if (argc != 1) + usage_ref(); + refname = strdup(argv[0]); + if (refname == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + + if (refname) + got_path_strip_trailing_slashes(refname); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + if (repo_path == NULL) { + error = got_worktree_open(&worktree, cwd); + if (error && error->code != GOT_ERR_NOT_WORKTREE) + goto done; + else + error = NULL; + if (worktree) { + repo_path = + strdup(got_worktree_get_repo_path(worktree)); + if (repo_path == NULL) + error = got_error_from_errno("strdup"); + if (error) + goto done; + } else { + repo_path = strdup(cwd); + if (repo_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + } + + error = got_repo_open(&repo, repo_path, NULL, pack_fds); + if (error != NULL) + goto done; + +#ifndef PROFILE + if (do_list) { + /* Remove "cpath" promise. */ + if (pledge("stdio rpath wpath flock proc exec sendfd unveil", + NULL) == -1) + err(1, "pledge"); + } +#endif + + error = apply_unveil(got_repo_get_path(repo), do_list, + worktree ? got_worktree_get_root_path(worktree) : NULL); + if (error) + goto done; + + if (do_list) + error = list_refs(repo, refname, sort_by_time); + else if (do_delete) + error = delete_ref_by_name(repo, refname); + else if (symref_target) + error = add_symref(repo, refname, symref_target); + else { + if (obj_arg == NULL) + usage_ref(); + error = add_ref(repo, refname, obj_arg); + } +done: + free(refname); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (worktree) + got_worktree_close(worktree); + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + free(cwd); + free(repo_path); + return error; +} + +__dead static void +usage_branch(void) +{ + fprintf(stderr, "usage: %s branch [-lnt] [-c commit] [-d name] " + "[-r repository-path] [name]\n", getprogname()); + exit(1); +} + +static const struct got_error * +list_branch(struct got_repository *repo, struct got_worktree *worktree, + struct got_reference *ref) +{ + const struct got_error *err = NULL; + const char *refname, *marker = " "; + char *refstr; + + refname = got_ref_get_name(ref); + if (worktree && strcmp(refname, + got_worktree_get_head_ref_name(worktree)) == 0) { + struct got_object_id *id = NULL; + + err = got_ref_resolve(&id, repo, ref); + if (err) + return err; + if (got_object_id_cmp(id, + got_worktree_get_base_commit_id(worktree)) == 0) + marker = "* "; + else + marker = "~ "; + free(id); + } + + if (strncmp(refname, "refs/heads/", 11) == 0) + refname += 11; + if (strncmp(refname, "refs/got/worktree/", 18) == 0) + refname += 18; + if (strncmp(refname, "refs/remotes/", 13) == 0) + refname += 13; + + refstr = got_ref_to_str(ref); + if (refstr == NULL) + return got_error_from_errno("got_ref_to_str"); + + printf("%s%s: %s\n", marker, refname, refstr); + free(refstr); + return NULL; +} + +static const struct got_error * +show_current_branch(struct got_repository *repo, struct got_worktree *worktree) +{ + const char *refname; + + if (worktree == NULL) + return got_error(GOT_ERR_NOT_WORKTREE); + + refname = got_worktree_get_head_ref_name(worktree); + + if (strncmp(refname, "refs/heads/", 11) == 0) + refname += 11; + if (strncmp(refname, "refs/got/worktree/", 18) == 0) + refname += 18; + + printf("%s\n", refname); + + return NULL; +} + +static const struct got_error * +list_branches(struct got_repository *repo, struct got_worktree *worktree, + int sort_by_time) +{ + static const struct got_error *err = NULL; + struct got_reflist_head refs; + struct got_reflist_entry *re; + struct got_reference *temp_ref = NULL; + int rebase_in_progress, histedit_in_progress; + + TAILQ_INIT(&refs); + + if (worktree) { + err = got_worktree_rebase_in_progress(&rebase_in_progress, + worktree); + if (err) + return err; + + err = got_worktree_histedit_in_progress(&histedit_in_progress, + worktree); + if (err) + return err; + + if (rebase_in_progress || histedit_in_progress) { + err = got_ref_open(&temp_ref, repo, + got_worktree_get_head_ref_name(worktree), 0); + if (err) + return err; + list_branch(repo, worktree, temp_ref); + got_ref_close(temp_ref); + } + } + + err = got_ref_list(&refs, repo, "refs/heads", sort_by_time ? + got_ref_cmp_by_commit_timestamp_descending : got_ref_cmp_by_name, + repo); + if (err) + return err; + + TAILQ_FOREACH(re, &refs, entry) + list_branch(repo, worktree, re->ref); + + got_ref_list_free(&refs); + + err = got_ref_list(&refs, repo, "refs/remotes", sort_by_time ? + got_ref_cmp_by_commit_timestamp_descending : got_ref_cmp_by_name, + repo); + if (err) + return err; + + TAILQ_FOREACH(re, &refs, entry) + list_branch(repo, worktree, re->ref); + + got_ref_list_free(&refs); + + return NULL; +} + +static const struct got_error * +delete_branch(struct got_repository *repo, struct got_worktree *worktree, + const char *branch_name) +{ + const struct got_error *err = NULL; + struct got_reference *ref = NULL; + char *refname, *remote_refname = NULL; + + if (strncmp(branch_name, "refs/", 5) == 0) + branch_name += 5; + if (strncmp(branch_name, "heads/", 6) == 0) + branch_name += 6; + else if (strncmp(branch_name, "remotes/", 8) == 0) + branch_name += 8; + + if (asprintf(&refname, "refs/heads/%s", branch_name) == -1) + return got_error_from_errno("asprintf"); + + if (asprintf(&remote_refname, "refs/remotes/%s", + branch_name) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + + err = got_ref_open(&ref, repo, refname, 0); + if (err) { + const struct got_error *err2; + if (err->code != GOT_ERR_NOT_REF) + goto done; + /* + * Keep 'err' intact such that if neither branch exists + * we report "refs/heads" rather than "refs/remotes" in + * our error message. + */ + err2 = got_ref_open(&ref, repo, remote_refname, 0); + if (err2) + goto done; + err = NULL; + } + + if (worktree && + strcmp(got_worktree_get_head_ref_name(worktree), + got_ref_get_name(ref)) == 0) { + err = got_error_msg(GOT_ERR_SAME_BRANCH, + "will not delete this work tree's current branch"); + goto done; + } + + err = delete_ref(repo, ref); +done: + if (ref) + got_ref_close(ref); + free(refname); + free(remote_refname); + return err; +} + +static const struct got_error * +add_branch(struct got_repository *repo, const char *branch_name, + struct got_object_id *base_commit_id) +{ + const struct got_error *err = NULL; + struct got_reference *ref = NULL; + char *refname = NULL; + + /* + * Don't let the user create a branch name with a leading '-'. + * While technically a valid reference name, this case is usually + * an unintended typo. + */ + if (branch_name[0] == '-') + return got_error_path(branch_name, GOT_ERR_REF_NAME_MINUS); + + if (strncmp(branch_name, "refs/heads/", 11) == 0) + branch_name += 11; + + if (asprintf(&refname, "refs/heads/%s", branch_name) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + + err = got_ref_open(&ref, repo, refname, 0); + if (err == NULL) { + err = got_error(GOT_ERR_BRANCH_EXISTS); + goto done; + } else if (err->code != GOT_ERR_NOT_REF) + goto done; + + err = got_ref_alloc(&ref, refname, base_commit_id); + if (err) + goto done; + + err = got_ref_write(ref, repo); +done: + if (ref) + got_ref_close(ref); + free(refname); + return err; +} + +static const struct got_error * +cmd_branch(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + char *cwd = NULL, *repo_path = NULL; + int ch, do_list = 0, do_show = 0, do_update = 1, sort_by_time = 0; + const char *delref = NULL, *commit_id_arg = NULL; + struct got_reference *ref = NULL; + struct got_pathlist_head paths; + struct got_object_id *commit_id = NULL; + char *commit_id_str = NULL; + int *pack_fds = NULL; + + TAILQ_INIT(&paths); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec " + "sendfd unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "c:d:lnr:t")) != -1) { + switch (ch) { + case 'c': + commit_id_arg = optarg; + break; + case 'd': + delref = optarg; + break; + case 'l': + do_list = 1; + break; + case 'n': + do_update = 0; + break; + case 'r': + repo_path = realpath(optarg, NULL); + if (repo_path == NULL) + return got_error_from_errno2("realpath", + optarg); + got_path_strip_trailing_slashes(repo_path); + break; + case 't': + sort_by_time = 1; + break; + default: + usage_branch(); + /* NOTREACHED */ + } + } + + if (do_list && delref) + option_conflict('l', 'd'); + if (sort_by_time && !do_list) + errx(1, "-t option requires -l option"); + + argc -= optind; + argv += optind; + + if (!do_list && !delref && argc == 0) + do_show = 1; + + if ((do_list || delref || do_show) && commit_id_arg != NULL) + errx(1, "-c option can only be used when creating a branch"); + + if (do_list || delref) { + if (argc > 0) + usage_branch(); + } else if (!do_show && argc != 1) + usage_branch(); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + if (repo_path == NULL) { + error = got_worktree_open(&worktree, cwd); + if (error && error->code != GOT_ERR_NOT_WORKTREE) + goto done; + else + error = NULL; + if (worktree) { + repo_path = + strdup(got_worktree_get_repo_path(worktree)); + if (repo_path == NULL) + error = got_error_from_errno("strdup"); + if (error) + goto done; + } else { + repo_path = strdup(cwd); + if (repo_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + } + + error = got_repo_open(&repo, repo_path, NULL, pack_fds); + if (error != NULL) + goto done; + +#ifndef PROFILE + if (do_list || do_show) { + /* Remove "cpath" promise. */ + if (pledge("stdio rpath wpath flock proc exec sendfd unveil", + NULL) == -1) + err(1, "pledge"); + } +#endif + + error = apply_unveil(got_repo_get_path(repo), do_list, + worktree ? got_worktree_get_root_path(worktree) : NULL); + if (error) + goto done; + + if (do_show) + error = show_current_branch(repo, worktree); + else if (do_list) + error = list_branches(repo, worktree, sort_by_time); + else if (delref) + error = delete_branch(repo, worktree, delref); + else { + struct got_reflist_head refs; + TAILQ_INIT(&refs); + error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, + NULL); + if (error) + goto done; + if (commit_id_arg == NULL) + commit_id_arg = worktree ? + got_worktree_get_head_ref_name(worktree) : + GOT_REF_HEAD; + error = got_repo_match_object_id(&commit_id, NULL, + commit_id_arg, GOT_OBJ_TYPE_COMMIT, &refs, repo); + got_ref_list_free(&refs); + if (error) + goto done; + error = add_branch(repo, argv[0], commit_id); + if (error) + goto done; + if (worktree && do_update) { + struct got_update_progress_arg upa; + char *branch_refname = NULL; + + error = got_object_id_str(&commit_id_str, commit_id); + if (error) + goto done; + error = get_worktree_paths_from_argv(&paths, 0, NULL, + worktree); + if (error) + goto done; + if (asprintf(&branch_refname, "refs/heads/%s", argv[0]) + == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + error = got_ref_open(&ref, repo, branch_refname, 0); + free(branch_refname); + if (error) + goto done; + error = switch_head_ref(ref, commit_id, worktree, + repo); + if (error) + goto done; + error = got_worktree_set_base_commit_id(worktree, repo, + commit_id); + if (error) + goto done; + memset(&upa, 0, sizeof(upa)); + error = got_worktree_checkout_files(worktree, &paths, + repo, update_progress, &upa, check_cancelled, + NULL); + if (error) + goto done; + if (upa.did_something) { + printf("Updated to %s: %s\n", + got_worktree_get_head_ref_name(worktree), + commit_id_str); + } + print_update_progress_stats(&upa); + } + } +done: + if (ref) + got_ref_close(ref); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (worktree) + got_worktree_close(worktree); + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + free(cwd); + free(repo_path); + free(commit_id); + free(commit_id_str); + got_pathlist_free(&paths, GOT_PATHLIST_FREE_PATH); + return error; +} + + +__dead static void +usage_tag(void) +{ + fprintf(stderr, "usage: %s tag [-lVv] [-c commit] [-m message] " + "[-r repository-path] [-s signer-id] name\n", getprogname()); + exit(1); +} + +#if 0 +static const struct got_error * +sort_tags(struct got_reflist_head *sorted, struct got_reflist_head *tags) +{ + const struct got_error *err = NULL; + struct got_reflist_entry *re, *se, *new; + struct got_object_id *re_id, *se_id; + struct got_tag_object *re_tag, *se_tag; + time_t re_time, se_time; + + STAILQ_FOREACH(re, tags, entry) { + se = STAILQ_FIRST(sorted); + if (se == NULL) { + err = got_reflist_entry_dup(&new, re); + if (err) + return err; + STAILQ_INSERT_HEAD(sorted, new, entry); + continue; + } else { + err = got_ref_resolve(&re_id, repo, re->ref); + if (err) + break; + err = got_object_open_as_tag(&re_tag, repo, re_id); + free(re_id); + if (err) + break; + re_time = got_object_tag_get_tagger_time(re_tag); + got_object_tag_close(re_tag); + } + + while (se) { + err = got_ref_resolve(&se_id, repo, re->ref); + if (err) + break; + err = got_object_open_as_tag(&se_tag, repo, se_id); + free(se_id); + if (err) + break; + se_time = got_object_tag_get_tagger_time(se_tag); + got_object_tag_close(se_tag); + + if (se_time > re_time) { + err = got_reflist_entry_dup(&new, re); + if (err) + return err; + STAILQ_INSERT_AFTER(sorted, se, new, entry); + break; + } + se = STAILQ_NEXT(se, entry); + continue; + } + } +done: + return err; +} +#endif + +static const struct got_error * +get_tag_refname(char **refname, const char *tag_name) +{ + const struct got_error *err; + + if (strncmp("refs/tags/", tag_name, 10) == 0) { + *refname = strdup(tag_name); + if (*refname == NULL) + return got_error_from_errno("strdup"); + } else if (asprintf(refname, "refs/tags/%s", tag_name) == -1) { + err = got_error_from_errno("asprintf"); + *refname = NULL; + return err; + } + + return NULL; +} + +static const struct got_error * +list_tags(struct got_repository *repo, const char *tag_name, int verify_tags, + const char *allowed_signers, const char *revoked_signers, int verbosity) +{ + static const struct got_error *err = NULL; + struct got_reflist_head refs; + struct got_reflist_entry *re; + char *wanted_refname = NULL; + int bad_sigs = 0; + + TAILQ_INIT(&refs); + + err = got_ref_list(&refs, repo, "refs/tags", got_ref_cmp_tags, repo); + if (err) + return err; + + if (tag_name) { + struct got_reference *ref; + err = get_tag_refname(&wanted_refname, tag_name); + if (err) + goto done; + /* Wanted tag reference should exist. */ + err = got_ref_open(&ref, repo, wanted_refname, 0); + if (err) + goto done; + got_ref_close(ref); + } + + TAILQ_FOREACH(re, &refs, entry) { + const char *refname; + char *refstr, *tagmsg0, *tagmsg, *line, *id_str, *datestr; + char datebuf[26]; + const char *tagger, *ssh_sig = NULL; + char *sig_msg = NULL; + time_t tagger_time; + struct got_object_id *id; + struct got_tag_object *tag; + struct got_commit_object *commit = NULL; + + refname = got_ref_get_name(re->ref); + if (strncmp(refname, "refs/tags/", 10) != 0 || + (wanted_refname && strcmp(refname, wanted_refname) != 0)) + continue; + refname += 10; + refstr = got_ref_to_str(re->ref); + if (refstr == NULL) { + err = got_error_from_errno("got_ref_to_str"); + break; + } + + err = got_ref_resolve(&id, repo, re->ref); + if (err) + break; + err = got_object_open_as_tag(&tag, repo, id); + if (err) { + if (err->code != GOT_ERR_OBJ_TYPE) { + free(id); + break; + } + /* "lightweight" tag */ + err = got_object_open_as_commit(&commit, repo, id); + if (err) { + free(id); + break; + } + tagger = got_object_commit_get_committer(commit); + tagger_time = + got_object_commit_get_committer_time(commit); + err = got_object_id_str(&id_str, id); + free(id); + if (err) + break; + } else { + free(id); + tagger = got_object_tag_get_tagger(tag); + tagger_time = got_object_tag_get_tagger_time(tag); + err = got_object_id_str(&id_str, + got_object_tag_get_object_id(tag)); + if (err) + break; + } + + if (tag && verify_tags) { + ssh_sig = got_sigs_get_tagmsg_ssh_signature( + got_object_tag_get_message(tag)); + if (ssh_sig && allowed_signers == NULL) { + err = got_error_msg( + GOT_ERR_VERIFY_TAG_SIGNATURE, + "SSH signature verification requires " + "setting allowed_signers in " + "got.conf(5)"); + break; + } + } + + printf("%stag %s %s\n", GOT_COMMIT_SEP_STR, refname, refstr); + free(refstr); + printf("from: %s\n", tagger); + datestr = get_datestr(&tagger_time, datebuf); + if (datestr) + printf("date: %s UTC\n", datestr); + if (commit) + printf("object: %s %s\n", GOT_OBJ_LABEL_COMMIT, id_str); + else { + switch (got_object_tag_get_object_type(tag)) { + case GOT_OBJ_TYPE_BLOB: + printf("object: %s %s\n", GOT_OBJ_LABEL_BLOB, + id_str); + break; + case GOT_OBJ_TYPE_TREE: + printf("object: %s %s\n", GOT_OBJ_LABEL_TREE, + id_str); + break; + case GOT_OBJ_TYPE_COMMIT: + printf("object: %s %s\n", GOT_OBJ_LABEL_COMMIT, + id_str); + break; + case GOT_OBJ_TYPE_TAG: + printf("object: %s %s\n", GOT_OBJ_LABEL_TAG, + id_str); + break; + default: + break; + } + } + free(id_str); + + if (ssh_sig) { + err = got_sigs_verify_tag_ssh(&sig_msg, tag, ssh_sig, + allowed_signers, revoked_signers, verbosity); + if (err && err->code == GOT_ERR_BAD_TAG_SIGNATURE) + bad_sigs = 1; + else if (err) + break; + printf("signature: %s", sig_msg); + free(sig_msg); + sig_msg = NULL; + } + + if (commit) { + err = got_object_commit_get_logmsg(&tagmsg0, commit); + if (err) + break; + got_object_commit_close(commit); + } else { + tagmsg0 = strdup(got_object_tag_get_message(tag)); + got_object_tag_close(tag); + if (tagmsg0 == NULL) { + err = got_error_from_errno("strdup"); + break; + } + } + + tagmsg = tagmsg0; + do { + line = strsep(&tagmsg, "\n"); + if (line) + printf(" %s\n", line); + } while (line); + free(tagmsg0); + } +done: + got_ref_list_free(&refs); + free(wanted_refname); + + if (err == NULL && bad_sigs) + err = got_error(GOT_ERR_BAD_TAG_SIGNATURE); + return err; +} + +static const struct got_error * +get_tag_message(char **tagmsg, char **tagmsg_path, const char *commit_id_str, + const char *tag_name, const char *repo_path) +{ + const struct got_error *err = NULL; + char *template = NULL, *initial_content = NULL; + char *editor = NULL; + int initial_content_len; + int fd = -1; + + if (asprintf(&template, GOT_TMPDIR_STR "/got-tagmsg") == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + + initial_content_len = asprintf(&initial_content, + "\n# tagging commit %s as %s\n", + commit_id_str, tag_name); + if (initial_content_len == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + + err = got_opentemp_named_fd(tagmsg_path, &fd, template, ""); + if (err) + goto done; + + if (write(fd, initial_content, initial_content_len) == -1) { + err = got_error_from_errno2("write", *tagmsg_path); + goto done; + } + if (close(fd) == -1) { + err = got_error_from_errno2("close", *tagmsg_path); + goto done; + } + fd = -1; + + err = get_editor(&editor); + if (err) + goto done; + err = edit_logmsg(tagmsg, editor, *tagmsg_path, initial_content, + initial_content_len, 1); +done: + free(initial_content); + free(template); + free(editor); + + if (fd != -1 && close(fd) == -1 && err == NULL) + err = got_error_from_errno2("close", *tagmsg_path); + + if (err) { + free(*tagmsg); + *tagmsg = NULL; + } + return err; +} + +static const struct got_error * +add_tag(struct got_repository *repo, const char *tagger, + const char *tag_name, const char *commit_arg, const char *tagmsg_arg, + const char *signer_id, int verbosity) +{ + const struct got_error *err = NULL; + struct got_object_id *commit_id = NULL, *tag_id = NULL; + char *label = NULL, *commit_id_str = NULL; + struct got_reference *ref = NULL; + char *refname = NULL, *tagmsg = NULL; + char *tagmsg_path = NULL, *tag_id_str = NULL; + int preserve_tagmsg = 0; + struct got_reflist_head refs; + + TAILQ_INIT(&refs); + + /* + * Don't let the user create a tag name with a leading '-'. + * While technically a valid reference name, this case is usually + * an unintended typo. + */ + if (tag_name[0] == '-') + return got_error_path(tag_name, GOT_ERR_REF_NAME_MINUS); + + err = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, NULL); + if (err) + goto done; + + err = got_repo_match_object_id(&commit_id, &label, commit_arg, + GOT_OBJ_TYPE_COMMIT, &refs, repo); + if (err) + goto done; + + err = got_object_id_str(&commit_id_str, commit_id); + if (err) + goto done; + + err = get_tag_refname(&refname, tag_name); + if (err) + goto done; + if (strncmp("refs/tags/", tag_name, 10) == 0) + tag_name += 10; + + err = got_ref_open(&ref, repo, refname, 0); + if (err == NULL) { + err = got_error(GOT_ERR_TAG_EXISTS); + goto done; + } else if (err->code != GOT_ERR_NOT_REF) + goto done; + + if (tagmsg_arg == NULL) { + err = get_tag_message(&tagmsg, &tagmsg_path, commit_id_str, + tag_name, got_repo_get_path(repo)); + if (err) { + if (err->code != GOT_ERR_COMMIT_MSG_EMPTY && + tagmsg_path != NULL) + preserve_tagmsg = 1; + goto done; + } + /* Editor is done; we can now apply unveil(2) */ + err = got_sigs_apply_unveil(); + if (err) + goto done; + err = apply_unveil(got_repo_get_path(repo), 0, NULL); + if (err) + goto done; + } + + err = got_object_tag_create(&tag_id, tag_name, commit_id, + tagger, time(NULL), tagmsg ? tagmsg : tagmsg_arg, signer_id, repo, + verbosity); + if (err) { + if (tagmsg_path) + preserve_tagmsg = 1; + goto done; + } + + err = got_ref_alloc(&ref, refname, tag_id); + if (err) { + if (tagmsg_path) + preserve_tagmsg = 1; + goto done; + } + + err = got_ref_write(ref, repo); + if (err) { + if (tagmsg_path) + preserve_tagmsg = 1; + goto done; + } + + err = got_object_id_str(&tag_id_str, tag_id); + if (err) { + if (tagmsg_path) + preserve_tagmsg = 1; + goto done; + } + printf("Created tag %s\n", tag_id_str); +done: + if (preserve_tagmsg) { + fprintf(stderr, "%s: tag message preserved in %s\n", + getprogname(), tagmsg_path); + } else if (tagmsg_path && unlink(tagmsg_path) == -1 && err == NULL) + err = got_error_from_errno2("unlink", tagmsg_path); + free(tag_id_str); + if (ref) + got_ref_close(ref); + free(commit_id); + free(commit_id_str); + free(refname); + free(tagmsg); + free(tagmsg_path); + got_ref_list_free(&refs); + return err; +} + +static const struct got_error * +cmd_tag(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + char *cwd = NULL, *repo_path = NULL, *commit_id_str = NULL; + char *gitconfig_path = NULL, *tagger = NULL; + char *allowed_signers = NULL, *revoked_signers = NULL; + const char *signer_id = NULL; + const char *tag_name = NULL, *commit_id_arg = NULL, *tagmsg = NULL; + int ch, do_list = 0, verify_tags = 0, verbosity = 0; + int *pack_fds = NULL; + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec " + "sendfd unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "c:lm:r:s:Vv")) != -1) { + switch (ch) { + case 'c': + commit_id_arg = optarg; + break; + case 'l': + do_list = 1; + break; + case 'm': + tagmsg = optarg; + break; + case 'r': + repo_path = realpath(optarg, NULL); + if (repo_path == NULL) { + error = got_error_from_errno2("realpath", + optarg); + goto done; + } + got_path_strip_trailing_slashes(repo_path); + break; + case 's': + signer_id = optarg; + break; + case 'V': + verify_tags = 1; + break; + case 'v': + if (verbosity < 0) + verbosity = 0; + else if (verbosity < 3) + verbosity++; + break; + default: + usage_tag(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (do_list || verify_tags) { + if (commit_id_arg != NULL) + errx(1, + "-c option can only be used when creating a tag"); + if (tagmsg) { + if (do_list) + option_conflict('l', 'm'); + else + option_conflict('V', 'm'); + } + if (signer_id) { + if (do_list) + option_conflict('l', 's'); + else + option_conflict('V', 's'); + } + if (argc > 1) + usage_tag(); + } else if (argc != 1) + usage_tag(); + + if (argc == 1) + tag_name = argv[0]; + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + if (repo_path == NULL) { + error = got_worktree_open(&worktree, cwd); + if (error && error->code != GOT_ERR_NOT_WORKTREE) + goto done; + else + error = NULL; + if (worktree) { + repo_path = + strdup(got_worktree_get_repo_path(worktree)); + if (repo_path == NULL) + error = got_error_from_errno("strdup"); + if (error) + goto done; + } else { + repo_path = strdup(cwd); + if (repo_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + } + + if (do_list || verify_tags) { + error = got_repo_open(&repo, repo_path, NULL, pack_fds); + if (error != NULL) + goto done; + error = get_allowed_signers(&allowed_signers, repo, worktree); + if (error) + goto done; + error = get_revoked_signers(&revoked_signers, repo, worktree); + if (error) + goto done; + if (worktree) { + /* Release work tree lock. */ + got_worktree_close(worktree); + worktree = NULL; + } + + /* + * Remove "cpath" promise unless needed for signature tmpfile + * creation. + */ + if (verify_tags) + got_sigs_apply_unveil(); + else { +#ifndef PROFILE + if (pledge("stdio rpath wpath flock proc exec sendfd " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + } + error = apply_unveil(got_repo_get_path(repo), 1, NULL); + if (error) + goto done; + error = list_tags(repo, tag_name, verify_tags, allowed_signers, + revoked_signers, verbosity); + } else { + error = get_gitconfig_path(&gitconfig_path); + if (error) + goto done; + error = got_repo_open(&repo, repo_path, gitconfig_path, + pack_fds); + if (error != NULL) + goto done; + + error = get_author(&tagger, repo, worktree); + if (error) + goto done; + if (signer_id == NULL) + signer_id = get_signer_id(repo, worktree); + + if (tagmsg) { + if (signer_id) { + error = got_sigs_apply_unveil(); + if (error) + goto done; + } + error = apply_unveil(got_repo_get_path(repo), 0, NULL); + if (error) + goto done; + } + + if (commit_id_arg == NULL) { + struct got_reference *head_ref; + struct got_object_id *commit_id; + error = got_ref_open(&head_ref, repo, + worktree ? got_worktree_get_head_ref_name(worktree) + : GOT_REF_HEAD, 0); + if (error) + goto done; + error = got_ref_resolve(&commit_id, repo, head_ref); + got_ref_close(head_ref); + if (error) + goto done; + error = got_object_id_str(&commit_id_str, commit_id); + free(commit_id); + if (error) + goto done; + } + + if (worktree) { + /* Release work tree lock. */ + got_worktree_close(worktree); + worktree = NULL; + } + + error = add_tag(repo, tagger, tag_name, + commit_id_str ? commit_id_str : commit_id_arg, tagmsg, + signer_id, verbosity); + } +done: + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (worktree) + got_worktree_close(worktree); + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + free(cwd); + free(repo_path); + free(gitconfig_path); + free(commit_id_str); + free(tagger); + free(allowed_signers); + free(revoked_signers); + return error; +} + +__dead static void +usage_add(void) +{ + fprintf(stderr, "usage: %s add [-IR] path ...\n", getprogname()); + exit(1); +} + +static const struct got_error * +add_progress(void *arg, unsigned char status, const char *path) +{ + while (path[0] == '/') + path++; + printf("%c %s\n", status, path); + return NULL; +} + +static const struct got_error * +cmd_add(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + char *cwd = NULL; + struct got_pathlist_head paths; + struct got_pathlist_entry *pe; + int ch, can_recurse = 0, no_ignores = 0; + int *pack_fds = NULL; + + TAILQ_INIT(&paths); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath flock proc exec sendfd unveil", + NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "IR")) != -1) { + switch (ch) { + case 'I': + no_ignores = 1; + break; + case 'R': + can_recurse = 1; + break; + default: + usage_add(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (argc < 1) + usage_add(); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_open(&worktree, cwd); + if (error) { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, "add", cwd); + goto done; + } + + error = got_repo_open(&repo, got_worktree_get_repo_path(worktree), + NULL, pack_fds); + if (error != NULL) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 1, + got_worktree_get_root_path(worktree)); + if (error) + goto done; + + error = get_worktree_paths_from_argv(&paths, argc, argv, worktree); + if (error) + goto done; + + if (!can_recurse) { + char *ondisk_path; + struct stat sb; + TAILQ_FOREACH(pe, &paths, entry) { + if (asprintf(&ondisk_path, "%s/%s", + got_worktree_get_root_path(worktree), + pe->path) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + if (lstat(ondisk_path, &sb) == -1) { + if (errno == ENOENT) { + free(ondisk_path); + continue; + } + error = got_error_from_errno2("lstat", + ondisk_path); + free(ondisk_path); + goto done; + } + free(ondisk_path); + if (S_ISDIR(sb.st_mode)) { + error = got_error_msg(GOT_ERR_BAD_PATH, + "adding directories requires -R option"); + goto done; + } + } + } + + error = got_worktree_schedule_add(worktree, &paths, add_progress, + NULL, repo, no_ignores); +done: + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (worktree) + got_worktree_close(worktree); + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + got_pathlist_free(&paths, GOT_PATHLIST_FREE_PATH); + free(cwd); + return error; +} + +__dead static void +usage_remove(void) +{ + fprintf(stderr, "usage: %s remove [-fkR] [-s status-codes] path ...\n", + getprogname()); + exit(1); +} + +static const struct got_error * +print_remove_status(void *arg, unsigned char status, + unsigned char staged_status, const char *path) +{ + while (path[0] == '/') + path++; + if (status == GOT_STATUS_NONEXISTENT) + return NULL; + if (status == staged_status && (status == GOT_STATUS_DELETE)) + status = GOT_STATUS_NO_CHANGE; + printf("%c%c %s\n", status, staged_status, path); + return NULL; +} + +static const struct got_error * +cmd_remove(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_worktree *worktree = NULL; + struct got_repository *repo = NULL; + const char *status_codes = NULL; + char *cwd = NULL; + struct got_pathlist_head paths; + struct got_pathlist_entry *pe; + int ch, delete_local_mods = 0, can_recurse = 0, keep_on_disk = 0, i; + int ignore_missing_paths = 0; + int *pack_fds = NULL; + + TAILQ_INIT(&paths); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath flock proc exec sendfd unveil", + NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "fkRs:")) != -1) { + switch (ch) { + case 'f': + delete_local_mods = 1; + ignore_missing_paths = 1; + break; + case 'k': + keep_on_disk = 1; + break; + case 'R': + can_recurse = 1; + break; + case 's': + for (i = 0; optarg[i] != '\0'; i++) { + switch (optarg[i]) { + case GOT_STATUS_MODIFY: + delete_local_mods = 1; + break; + case GOT_STATUS_MISSING: + ignore_missing_paths = 1; + break; + default: + errx(1, "invalid status code '%c'", + optarg[i]); + } + } + status_codes = optarg; + break; + default: + usage_remove(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (argc < 1) + usage_remove(); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_open(&worktree, cwd); + if (error) { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, "remove", cwd); + goto done; + } + + error = got_repo_open(&repo, got_worktree_get_repo_path(worktree), + NULL, pack_fds); + if (error) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 1, + got_worktree_get_root_path(worktree)); + if (error) + goto done; + + error = get_worktree_paths_from_argv(&paths, argc, argv, worktree); + if (error) + goto done; + + if (!can_recurse) { + char *ondisk_path; + struct stat sb; + TAILQ_FOREACH(pe, &paths, entry) { + if (asprintf(&ondisk_path, "%s/%s", + got_worktree_get_root_path(worktree), + pe->path) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + if (lstat(ondisk_path, &sb) == -1) { + if (errno == ENOENT) { + free(ondisk_path); + continue; + } + error = got_error_from_errno2("lstat", + ondisk_path); + free(ondisk_path); + goto done; + } + free(ondisk_path); + if (S_ISDIR(sb.st_mode)) { + error = got_error_msg(GOT_ERR_BAD_PATH, + "removing directories requires -R option"); + goto done; + } + } + } + + error = got_worktree_schedule_delete(worktree, &paths, + delete_local_mods, status_codes, print_remove_status, NULL, + repo, keep_on_disk, ignore_missing_paths); +done: + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (worktree) + got_worktree_close(worktree); + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + got_pathlist_free(&paths, GOT_PATHLIST_FREE_PATH); + free(cwd); + return error; +} + +__dead static void +usage_patch(void) +{ + fprintf(stderr, "usage: %s patch [-nR] [-c commit] [-p strip-count] " + "[patchfile]\n", getprogname()); + exit(1); +} + +static const struct got_error * +patch_from_stdin(int *patchfd) +{ + const struct got_error *err = NULL; + ssize_t r; + char buf[BUFSIZ]; + sig_t sighup, sigint, sigquit; + + *patchfd = got_opentempfd(); + if (*patchfd == -1) + return got_error_from_errno("got_opentempfd"); + + sighup = signal(SIGHUP, SIG_DFL); + sigint = signal(SIGINT, SIG_DFL); + sigquit = signal(SIGQUIT, SIG_DFL); + + for (;;) { + r = read(0, buf, sizeof(buf)); + if (r == -1) { + err = got_error_from_errno("read"); + break; + } + if (r == 0) + break; + if (write(*patchfd, buf, r) == -1) { + err = got_error_from_errno("write"); + break; + } + } + + signal(SIGHUP, sighup); + signal(SIGINT, sigint); + signal(SIGQUIT, sigquit); + + if (err == NULL && lseek(*patchfd, 0, SEEK_SET) == -1) + err = got_error_from_errno("lseek"); + + if (err != NULL) { + close(*patchfd); + *patchfd = -1; + } + + return err; +} + +struct got_patch_progress_arg { + int did_something; + int conflicts; + int rejects; +}; + +static const struct got_error * +patch_progress(void *arg, const char *old, const char *new, + unsigned char status, const struct got_error *error, int old_from, + int old_lines, int new_from, int new_lines, int offset, + int ws_mangled, const struct got_error *hunk_err) +{ + const char *path = new == NULL ? old : new; + struct got_patch_progress_arg *a = arg; + + while (*path == '/') + path++; + + if (status != GOT_STATUS_NO_CHANGE && + status != 0 /* per-hunk progress */) { + printf("%c %s\n", status, path); + a->did_something = 1; + } + + if (hunk_err == NULL) { + if (status == GOT_STATUS_CANNOT_UPDATE) + a->rejects++; + else if (status == GOT_STATUS_CONFLICT) + a->conflicts++; + } + + if (error != NULL) + fprintf(stderr, "%s: %s\n", getprogname(), error->msg); + + if (offset != 0 || hunk_err != NULL || ws_mangled) { + printf("@@ -%d,%d +%d,%d @@ ", old_from, + old_lines, new_from, new_lines); + if (hunk_err != NULL) + printf("%s\n", hunk_err->msg); + else if (offset != 0) + printf("applied with offset %d\n", offset); + else + printf("hunk contains mangled whitespace\n"); + } + + return NULL; +} + +static void +print_patch_progress_stats(struct got_patch_progress_arg *ppa) +{ + if (!ppa->did_something) + return; + + if (ppa->conflicts > 0) + printf("Files with merge conflicts: %d\n", ppa->conflicts); + + if (ppa->rejects > 0) { + printf("Files where patch failed to apply: %d\n", + ppa->rejects); + } +} + +static const struct got_error * +cmd_patch(int argc, char *argv[]) +{ + const struct got_error *error = NULL, *close_error = NULL; + struct got_worktree *worktree = NULL; + struct got_repository *repo = NULL; + struct got_reflist_head refs; + struct got_object_id *commit_id = NULL; + const char *commit_id_str = NULL; + struct stat sb; + const char *errstr; + char *cwd = NULL; + int ch, nop = 0, strip = -1, reverse = 0; + int patchfd; + int *pack_fds = NULL; + struct got_patch_progress_arg ppa; + + TAILQ_INIT(&refs); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr proc exec sendfd flock " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "c:np:R")) != -1) { + switch (ch) { + case 'c': + commit_id_str = optarg; + break; + case 'n': + nop = 1; + break; + case 'p': + strip = strtonum(optarg, 0, INT_MAX, &errstr); + if (errstr != NULL) + errx(1, "pathname strip count is %s: %s", + errstr, optarg); + break; + case 'R': + reverse = 1; + break; + default: + usage_patch(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (argc == 0) { + error = patch_from_stdin(&patchfd); + if (error) + return error; + } else if (argc == 1) { + patchfd = open(argv[0], O_RDONLY); + if (patchfd == -1) { + error = got_error_from_errno2("open", argv[0]); + return error; + } + if (fstat(patchfd, &sb) == -1) { + error = got_error_from_errno2("fstat", argv[0]); + goto done; + } + if (!S_ISREG(sb.st_mode)) { + error = got_error_path(argv[0], GOT_ERR_BAD_FILETYPE); + goto done; + } + } else + usage_patch(); + + if ((cwd = getcwd(NULL, 0)) == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_open(&worktree, cwd); + if (error != NULL) + goto done; + + const char *repo_path = got_worktree_get_repo_path(worktree); + error = got_repo_open(&repo, repo_path, NULL, pack_fds); + if (error != NULL) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 0, + got_worktree_get_root_path(worktree)); + if (error != NULL) + goto done; + + error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, NULL); + if (error) + goto done; + + if (commit_id_str != NULL) { + error = got_repo_match_object_id(&commit_id, NULL, + commit_id_str, GOT_OBJ_TYPE_COMMIT, &refs, repo); + if (error) + goto done; + } + + memset(&ppa, 0, sizeof(ppa)); + error = got_patch(patchfd, worktree, repo, nop, strip, reverse, + commit_id, patch_progress, &ppa, check_cancelled, NULL); + print_patch_progress_stats(&ppa); +done: + got_ref_list_free(&refs); + free(commit_id); + if (repo) { + close_error = got_repo_close(repo); + if (error == NULL) + error = close_error; + } + if (worktree != NULL) { + close_error = got_worktree_close(worktree); + if (error == NULL) + error = close_error; + } + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + free(cwd); + return error; +} + +__dead static void +usage_revert(void) +{ + fprintf(stderr, "usage: %s revert [-pR] [-F response-script] path ...\n", + getprogname()); + exit(1); +} + +static const struct got_error * +revert_progress(void *arg, unsigned char status, const char *path) +{ + if (status == GOT_STATUS_UNVERSIONED) + return NULL; + + while (path[0] == '/') + path++; + printf("%c %s\n", status, path); + return NULL; +} + +struct choose_patch_arg { + FILE *patch_script_file; + const char *action; +}; + +static const struct got_error * +show_change(unsigned char status, const char *path, FILE *patch_file, int n, + int nchanges, const char *action) +{ + const struct got_error *err; + char *line = NULL; + size_t linesize = 0; + ssize_t linelen; + + switch (status) { + case GOT_STATUS_ADD: + printf("A %s\n%s this addition? [y/n] ", path, action); + break; + case GOT_STATUS_DELETE: + printf("D %s\n%s this deletion? [y/n] ", path, action); + break; + case GOT_STATUS_MODIFY: + if (fseek(patch_file, 0L, SEEK_SET) == -1) + return got_error_from_errno("fseek"); + printf(GOT_COMMIT_SEP_STR); + while ((linelen = getline(&line, &linesize, patch_file)) != -1) + printf("%s", line); + if (linelen == -1 && ferror(patch_file)) { + err = got_error_from_errno("getline"); + free(line); + return err; + } + free(line); + printf(GOT_COMMIT_SEP_STR); + printf("M %s (change %d of %d)\n%s this change? [y/n/q] ", + path, n, nchanges, action); + break; + default: + return got_error_path(path, GOT_ERR_FILE_STATUS); + } + + fflush(stdout); + return NULL; +} + +static const struct got_error * +choose_patch(int *choice, void *arg, unsigned char status, const char *path, + FILE *patch_file, int n, int nchanges) +{ + const struct got_error *err = NULL; + char *line = NULL; + size_t linesize = 0; + ssize_t linelen; + int resp = ' '; + struct choose_patch_arg *a = arg; + + *choice = GOT_PATCH_CHOICE_NONE; + + if (a->patch_script_file) { + char *nl; + err = show_change(status, path, patch_file, n, nchanges, + a->action); + if (err) + return err; + linelen = getline(&line, &linesize, a->patch_script_file); + if (linelen == -1) { + if (ferror(a->patch_script_file)) + return got_error_from_errno("getline"); + return NULL; + } + nl = strchr(line, '\n'); + if (nl) + *nl = '\0'; + if (strcmp(line, "y") == 0) { + *choice = GOT_PATCH_CHOICE_YES; + printf("y\n"); + } else if (strcmp(line, "n") == 0) { + *choice = GOT_PATCH_CHOICE_NO; + printf("n\n"); + } else if (strcmp(line, "q") == 0 && + status == GOT_STATUS_MODIFY) { + *choice = GOT_PATCH_CHOICE_QUIT; + printf("q\n"); + } else + printf("invalid response '%s'\n", line); + free(line); + return NULL; + } + + while (resp != 'y' && resp != 'n' && resp != 'q') { + err = show_change(status, path, patch_file, n, nchanges, + a->action); + if (err) + return err; + resp = getchar(); + if (resp == '\n') + resp = getchar(); + if (status == GOT_STATUS_MODIFY) { + if (resp != 'y' && resp != 'n' && resp != 'q') { + printf("invalid response '%c'\n", resp); + resp = ' '; + } + } else if (resp != 'y' && resp != 'n') { + printf("invalid response '%c'\n", resp); + resp = ' '; + } + } + + if (resp == 'y') + *choice = GOT_PATCH_CHOICE_YES; + else if (resp == 'n') + *choice = GOT_PATCH_CHOICE_NO; + else if (resp == 'q' && status == GOT_STATUS_MODIFY) + *choice = GOT_PATCH_CHOICE_QUIT; + + return NULL; +} + +struct wt_commitable_path_arg { + struct got_pathlist_head *commit_paths; + int *has_changes; +}; + +/* + * Shortcut work tree status callback to determine if the set of paths scanned + * has at least one versioned path that is being modified and, if not NULL, is + * in the arg->commit_paths list. Set arg and return GOT_ERR_FILE_MODIFIED as + * soon as a path is passed with a status that satisfies this criteria. + */ +static const struct got_error * +worktree_has_commitable_path(void *arg, unsigned char status, + unsigned char staged_status, const char *path, + struct got_object_id *blob_id, struct got_object_id *staged_blob_id, + struct got_object_id *commit_id, int dirfd, const char *de_name) +{ + struct wt_commitable_path_arg *a = arg; + + if (status == staged_status && (status == GOT_STATUS_DELETE)) + status = GOT_STATUS_NO_CHANGE; + + if (!(status == GOT_STATUS_NO_CHANGE || + status == GOT_STATUS_UNVERSIONED) || + staged_status != GOT_STATUS_NO_CHANGE) { + if (a->commit_paths != NULL) { + struct got_pathlist_entry *pe; + + TAILQ_FOREACH(pe, a->commit_paths, entry) { + if (strncmp(path, pe->path, + pe->path_len) == 0) { + *a->has_changes = 1; + break; + } + } + } else + *a->has_changes = 1; + + if (*a->has_changes) + return got_error(GOT_ERR_FILE_MODIFIED); + } + + return NULL; +} + +/* + * Check that the changeset of the commit identified by id is + * comprised of at least one modified path that is being committed. + */ +static const struct got_error * +commit_path_changed_in_worktree(struct wt_commitable_path_arg *wcpa, + struct got_object_id *id, struct got_worktree *worktree, + struct got_repository *repo) +{ + const struct got_error *err; + struct got_pathlist_head paths; + struct got_commit_object *commit = NULL, *pcommit = NULL; + struct got_tree_object *tree = NULL, *ptree = NULL; + struct got_object_qid *pid; + + TAILQ_INIT(&paths); + + err = got_object_open_as_commit(&commit, repo, id); + if (err) + goto done; + + err = got_object_open_as_tree(&tree, repo, + got_object_commit_get_tree_id(commit)); + if (err) + goto done; + + pid = STAILQ_FIRST(got_object_commit_get_parent_ids(commit)); + if (pid != NULL) { + err = got_object_open_as_commit(&pcommit, repo, &pid->id); + if (err) + goto done; + + err = got_object_open_as_tree(&ptree, repo, + got_object_commit_get_tree_id(pcommit)); + if (err) + goto done; + } + + err = got_diff_tree(ptree, tree, NULL, NULL, -1, -1, "", "", repo, + got_diff_tree_collect_changed_paths, &paths, 0); + if (err) + goto done; + + err = got_worktree_status(worktree, &paths, repo, 0, + worktree_has_commitable_path, wcpa, check_cancelled, NULL); + if (err && err->code == GOT_ERR_FILE_MODIFIED) { + /* + * At least one changed path in the referenced commit is + * modified in the work tree, that's all we need to know! + */ + err = NULL; + } + +done: + got_pathlist_free(&paths, GOT_PATHLIST_FREE_ALL); + if (commit) + got_object_commit_close(commit); + if (pcommit) + got_object_commit_close(pcommit); + if (tree) + got_object_tree_close(tree); + if (ptree) + got_object_tree_close(ptree); + return err; +} + +/* + * Remove any "logmsg" reference comprised entirely of paths that have + * been reverted in this work tree. If any path in the logmsg ref changeset + * remains in a changed state in the worktree, do not remove the reference. + */ +static const struct got_error * +rm_logmsg_ref(struct got_worktree *worktree, struct got_repository *repo) +{ + const struct got_error *err; + struct got_reflist_head refs; + struct got_reflist_entry *re; + struct got_commit_object *commit = NULL; + struct got_object_id *commit_id = NULL; + struct wt_commitable_path_arg wcpa; + char *uuidstr = NULL; + + TAILQ_INIT(&refs); + + err = got_worktree_get_uuid(&uuidstr, worktree); + if (err) + goto done; + + err = got_ref_list(&refs, repo, "refs/got/worktree", + got_ref_cmp_by_name, repo); + if (err) + goto done; + + TAILQ_FOREACH(re, &refs, entry) { + const char *refname; + int has_changes = 0; + + refname = got_ref_get_name(re->ref); + + if (!strncmp(refname, GOT_WORKTREE_CHERRYPICK_REF_PREFIX, + GOT_WORKTREE_CHERRYPICK_REF_PREFIX_LEN)) + refname += GOT_WORKTREE_CHERRYPICK_REF_PREFIX_LEN + 1; + else if (!strncmp(refname, GOT_WORKTREE_BACKOUT_REF_PREFIX, + GOT_WORKTREE_BACKOUT_REF_PREFIX_LEN)) + refname += GOT_WORKTREE_BACKOUT_REF_PREFIX_LEN + 1; + else + continue; + + if (strncmp(refname, uuidstr, GOT_WORKTREE_UUID_STRLEN) == 0) + refname += GOT_WORKTREE_UUID_STRLEN + 1; /* skip '-' */ + else + continue; + + err = got_repo_match_object_id(&commit_id, NULL, refname, + GOT_OBJ_TYPE_COMMIT, NULL, repo); + if (err) + goto done; + + err = got_object_open_as_commit(&commit, repo, commit_id); + if (err) + goto done; + + wcpa.commit_paths = NULL; + wcpa.has_changes = &has_changes; + + err = commit_path_changed_in_worktree(&wcpa, commit_id, + worktree, repo); + if (err) + goto done; + + if (!has_changes) { + err = got_ref_delete(re->ref, repo); + if (err) + goto done; + } + + got_object_commit_close(commit); + commit = NULL; + free(commit_id); + commit_id = NULL; + } + +done: + free(uuidstr); + free(commit_id); + got_ref_list_free(&refs); + if (commit) + got_object_commit_close(commit); + return err; +} + +static const struct got_error * +cmd_revert(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_worktree *worktree = NULL; + struct got_repository *repo = NULL; + char *cwd = NULL, *path = NULL; + struct got_pathlist_head paths; + struct got_pathlist_entry *pe; + int ch, can_recurse = 0, pflag = 0; + FILE *patch_script_file = NULL; + const char *patch_script_path = NULL; + struct choose_patch_arg cpa; + int *pack_fds = NULL; + + TAILQ_INIT(&paths); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "F:pR")) != -1) { + switch (ch) { + case 'F': + patch_script_path = optarg; + break; + case 'p': + pflag = 1; + break; + case 'R': + can_recurse = 1; + break; + default: + usage_revert(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (argc < 1) + usage_revert(); + if (patch_script_path && !pflag) + errx(1, "-F option can only be used together with -p option"); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_open(&worktree, cwd); + if (error) { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, "revert", cwd); + goto done; + } + + error = got_repo_open(&repo, got_worktree_get_repo_path(worktree), + NULL, pack_fds); + if (error != NULL) + goto done; + + if (patch_script_path) { + patch_script_file = fopen(patch_script_path, "re"); + if (patch_script_file == NULL) { + error = got_error_from_errno2("fopen", + patch_script_path); + goto done; + } + } + + /* + * XXX "c" perm needed on repo dir to delete merge references. + */ + error = apply_unveil(got_repo_get_path(repo), 0, + got_worktree_get_root_path(worktree)); + if (error) + goto done; + + error = get_worktree_paths_from_argv(&paths, argc, argv, worktree); + if (error) + goto done; + + if (!can_recurse) { + char *ondisk_path; + struct stat sb; + TAILQ_FOREACH(pe, &paths, entry) { + if (asprintf(&ondisk_path, "%s/%s", + got_worktree_get_root_path(worktree), + pe->path) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + if (lstat(ondisk_path, &sb) == -1) { + if (errno == ENOENT) { + free(ondisk_path); + continue; + } + error = got_error_from_errno2("lstat", + ondisk_path); + free(ondisk_path); + goto done; + } + free(ondisk_path); + if (S_ISDIR(sb.st_mode)) { + error = got_error_msg(GOT_ERR_BAD_PATH, + "reverting directories requires -R option"); + goto done; + } + } + } + + cpa.patch_script_file = patch_script_file; + cpa.action = "revert"; + error = got_worktree_revert(worktree, &paths, revert_progress, NULL, + pflag ? choose_patch : NULL, &cpa, repo); + + error = rm_logmsg_ref(worktree, repo); +done: + if (patch_script_file && fclose(patch_script_file) == EOF && + error == NULL) + error = got_error_from_errno2("fclose", patch_script_path); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (worktree) + got_worktree_close(worktree); + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + got_pathlist_free(&paths, GOT_PATHLIST_FREE_PATH); + free(path); + free(cwd); + return error; +} + +__dead static void +usage_commit(void) +{ + fprintf(stderr, "usage: %s commit [-CNnS] [-A author] [-F path] " + "[-m message] [path ...]\n", getprogname()); + exit(1); +} + +struct collect_commit_logmsg_arg { + const char *cmdline_log; + const char *prepared_log; + const char *merged_log; + int non_interactive; + const char *editor; + const char *worktree_path; + const char *branch_name; + const char *repo_path; + char *logmsg_path; + +}; + +static const struct got_error * +read_prepared_logmsg(char **logmsg, const char *path) +{ + const struct got_error *err = NULL; + FILE *f = NULL; + struct stat sb; + size_t r; + + *logmsg = NULL; + memset(&sb, 0, sizeof(sb)); + + f = fopen(path, "re"); + if (f == NULL) + return got_error_from_errno2("fopen", path); + + if (fstat(fileno(f), &sb) == -1) { + err = got_error_from_errno2("fstat", path); + goto done; + } + if (sb.st_size == 0) { + err = got_error(GOT_ERR_COMMIT_MSG_EMPTY); + goto done; + } + + *logmsg = malloc(sb.st_size + 1); + if (*logmsg == NULL) { + err = got_error_from_errno("malloc"); + goto done; + } + + r = fread(*logmsg, 1, sb.st_size, f); + if (r != sb.st_size) { + if (ferror(f)) + err = got_error_from_errno2("fread", path); + else + err = got_error(GOT_ERR_IO); + goto done; + } + (*logmsg)[sb.st_size] = '\0'; +done: + if (fclose(f) == EOF && err == NULL) + err = got_error_from_errno2("fclose", path); + if (err) { + free(*logmsg); + *logmsg = NULL; + } + return err; +} + +static const struct got_error * +collect_commit_logmsg(struct got_pathlist_head *commitable_paths, + const char *diff_path, char **logmsg, void *arg) +{ + char *initial_content = NULL; + struct got_pathlist_entry *pe; + const struct got_error *err = NULL; + char *template = NULL; + char *prepared_msg = NULL, *merged_msg = NULL; + struct collect_commit_logmsg_arg *a = arg; + int initial_content_len; + int fd = -1; + size_t len; + + /* if a message was specified on the command line, just use it */ + if (a->cmdline_log != NULL && *a->cmdline_log != '\0') { + len = strlen(a->cmdline_log) + 1; + *logmsg = malloc(len + 1); + if (*logmsg == NULL) + return got_error_from_errno("malloc"); + strlcpy(*logmsg, a->cmdline_log, len); + return NULL; + } else if (a->prepared_log != NULL && a->non_interactive) + return read_prepared_logmsg(logmsg, a->prepared_log); + + if (asprintf(&template, "%s/logmsg", a->worktree_path) == -1) + return got_error_from_errno("asprintf"); + + err = got_opentemp_named_fd(&a->logmsg_path, &fd, template, ""); + if (err) + goto done; + + if (a->prepared_log) { + err = read_prepared_logmsg(&prepared_msg, a->prepared_log); + if (err) + goto done; + } else if (a->merged_log) { + err = read_prepared_logmsg(&merged_msg, a->merged_log); + if (err) + goto done; + } + + initial_content_len = asprintf(&initial_content, + "%s%s\n# changes to be committed on branch %s:\n", + prepared_msg ? prepared_msg : "", + merged_msg ? merged_msg : "", a->branch_name); + if (initial_content_len == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + + if (write(fd, initial_content, initial_content_len) == -1) { + err = got_error_from_errno2("write", a->logmsg_path); + goto done; + } + + TAILQ_FOREACH(pe, commitable_paths, entry) { + struct got_commitable *ct = pe->data; + dprintf(fd, "# %c %s\n", + got_commitable_get_status(ct), + got_commitable_get_path(ct)); + } + + if (diff_path) { + dprintf(fd, "# detailed changes can be viewed in %s\n", + diff_path); + } + + if (close(fd) == -1) { + err = got_error_from_errno2("close", a->logmsg_path); + goto done; + } + fd = -1; + + err = edit_logmsg(logmsg, a->editor, a->logmsg_path, initial_content, + initial_content_len, a->prepared_log ? 0 : 1); +done: + free(initial_content); + free(template); + free(prepared_msg); + free(merged_msg); + + if (fd != -1 && close(fd) == -1 && err == NULL) + err = got_error_from_errno2("close", a->logmsg_path); + + /* Editor is done; we can now apply unveil(2) */ + if (err == NULL) + err = apply_unveil(a->repo_path, 0, a->worktree_path); + if (err) { + free(*logmsg); + *logmsg = NULL; + } + return err; +} + +static const struct got_error * +cat_logmsg(FILE *f, struct got_commit_object *commit, const char *idstr, + const char *type, int has_content) +{ + const struct got_error *err = NULL; + char *logmsg = NULL; + + err = got_object_commit_get_logmsg(&logmsg, commit); + if (err) + return err; + + if (fprintf(f, "%s# log message of %s commit %s:%s", + has_content ? "\n" : "", type, idstr, logmsg) < 0) + err = got_ferror(f, GOT_ERR_IO); + + free(logmsg); + return err; +} + +/* + * Lookup "logmsg" references of backed-out and cherrypicked commits + * belonging to the current work tree. If found, and the worktree has + * at least one modified file that was changed in the referenced commit, + * add its log message to a new temporary file at *logmsg_path. + * Add all refs found to matched_refs to be scheduled for removal on + * successful commit. + */ +static const struct got_error * +lookup_logmsg_ref(char **logmsg_path, struct got_pathlist_head *paths, + struct got_reflist_head *matched_refs, struct got_worktree *worktree, + struct got_repository *repo) +{ + const struct got_error *err; + struct got_commit_object *commit = NULL; + struct got_object_id *id = NULL; + struct got_reflist_head refs; + struct got_reflist_entry *re, *re_match; + FILE *f = NULL; + char *uuidstr = NULL; + int added_logmsg = 0; + + TAILQ_INIT(&refs); + + *logmsg_path = NULL; + + err = got_worktree_get_uuid(&uuidstr, worktree); + if (err) + goto done; + + err = got_ref_list(&refs, repo, "refs/got/worktree", + got_ref_cmp_by_name, repo); + if (err) + goto done; + + TAILQ_FOREACH(re, &refs, entry) { + const char *refname, *type; + struct wt_commitable_path_arg wcpa; + int add_logmsg = 0; + + refname = got_ref_get_name(re->ref); + + if (strncmp(refname, GOT_WORKTREE_CHERRYPICK_REF_PREFIX, + GOT_WORKTREE_CHERRYPICK_REF_PREFIX_LEN) == 0) { + refname += GOT_WORKTREE_CHERRYPICK_REF_PREFIX_LEN + 1; + type = "cherrypicked"; + } else if (strncmp(refname, GOT_WORKTREE_BACKOUT_REF_PREFIX, + GOT_WORKTREE_BACKOUT_REF_PREFIX_LEN) == 0) { + refname += GOT_WORKTREE_BACKOUT_REF_PREFIX_LEN + 1; + type = "backed-out"; + } else + continue; + + if (strncmp(refname, uuidstr, GOT_WORKTREE_UUID_STRLEN) == 0) + refname += GOT_WORKTREE_UUID_STRLEN + 1; /* skip '-' */ + else + continue; + + err = got_repo_match_object_id(&id, NULL, refname, + GOT_OBJ_TYPE_COMMIT, NULL, repo); + if (err) + goto done; + + err = got_object_open_as_commit(&commit, repo, id); + if (err) + goto done; + + wcpa.commit_paths = paths; + wcpa.has_changes = &add_logmsg; + + err = commit_path_changed_in_worktree(&wcpa, id, + worktree, repo); + if (err) + goto done; + + if (add_logmsg) { + if (f == NULL) { + err = got_opentemp_named(logmsg_path, &f, + "got-commit-logmsg", ""); + if (err) + goto done; + } + err = cat_logmsg(f, commit, refname, type, + added_logmsg); + if (err) + goto done; + if (!added_logmsg) + ++added_logmsg; + + err = got_reflist_entry_dup(&re_match, re); + if (err) + goto done; + TAILQ_INSERT_HEAD(matched_refs, re_match, entry); + } + + got_object_commit_close(commit); + commit = NULL; + free(id); + id = NULL; + } + +done: + free(id); + free(uuidstr); + got_ref_list_free(&refs); + if (commit) + got_object_commit_close(commit); + if (f && fclose(f) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + if (!added_logmsg) { + if (*logmsg_path && unlink(*logmsg_path) != 0 && err == NULL) + err = got_error_from_errno2("unlink", *logmsg_path); + *logmsg_path = NULL; + } + return err; +} + +static const struct got_error * +cmd_commit(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_worktree *worktree = NULL; + struct got_repository *repo = NULL; + char *cwd = NULL, *id_str = NULL; + struct got_object_id *id = NULL; + const char *logmsg = NULL; + char *prepared_logmsg = NULL, *merged_logmsg = NULL; + struct collect_commit_logmsg_arg cl_arg; + const char *author = NULL; + char *gitconfig_path = NULL, *editor = NULL, *committer = NULL; + int ch, rebase_in_progress, histedit_in_progress, preserve_logmsg = 0; + int allow_bad_symlinks = 0, non_interactive = 0, merge_in_progress = 0; + int show_diff = 1, commit_conflicts = 0; + struct got_pathlist_head paths; + struct got_reflist_head refs; + struct got_reflist_entry *re; + int *pack_fds = NULL; + + TAILQ_INIT(&refs); + TAILQ_INIT(&paths); + cl_arg.logmsg_path = NULL; + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "A:CF:m:NnS")) != -1) { + switch (ch) { + case 'A': + author = optarg; + error = valid_author(author); + if (error) + return error; + break; + case 'C': + commit_conflicts = 1; + break; + case 'F': + if (logmsg != NULL) + option_conflict('F', 'm'); + prepared_logmsg = realpath(optarg, NULL); + if (prepared_logmsg == NULL) + return got_error_from_errno2("realpath", + optarg); + break; + case 'm': + if (prepared_logmsg) + option_conflict('m', 'F'); + logmsg = optarg; + break; + case 'N': + non_interactive = 1; + break; + case 'n': + show_diff = 0; + break; + case 'S': + allow_bad_symlinks = 1; + break; + default: + usage_commit(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_open(&worktree, cwd); + if (error) { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, "commit", cwd); + goto done; + } + + error = got_worktree_rebase_in_progress(&rebase_in_progress, worktree); + if (error) + goto done; + if (rebase_in_progress) { + error = got_error(GOT_ERR_REBASING); + goto done; + } + + error = got_worktree_histedit_in_progress(&histedit_in_progress, + worktree); + if (error) + goto done; + + error = get_gitconfig_path(&gitconfig_path); + if (error) + goto done; + error = got_repo_open(&repo, got_worktree_get_repo_path(worktree), + gitconfig_path, pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_merge_in_progress(&merge_in_progress, worktree, repo); + if (error) + goto done; + if (merge_in_progress) { + error = got_error(GOT_ERR_MERGE_BUSY); + goto done; + } + + error = get_author(&committer, repo, worktree); + if (error) + goto done; + + if (author == NULL) + author = committer; + + /* + * unveil(2) traverses exec(2); if an editor is used we have + * to apply unveil after the log message has been written. + */ + if (logmsg == NULL || strlen(logmsg) == 0) + error = get_editor(&editor); + else + error = apply_unveil(got_repo_get_path(repo), 0, + got_worktree_get_root_path(worktree)); + if (error) + goto done; + + error = get_worktree_paths_from_argv(&paths, argc, argv, worktree); + if (error) + goto done; + + if (prepared_logmsg == NULL) { + error = lookup_logmsg_ref(&merged_logmsg, + argc > 0 ? &paths : NULL, &refs, worktree, repo); + if (error) + goto done; + } + + cl_arg.editor = editor; + cl_arg.cmdline_log = logmsg; + cl_arg.prepared_log = prepared_logmsg; + cl_arg.merged_log = merged_logmsg; + cl_arg.non_interactive = non_interactive; + cl_arg.worktree_path = got_worktree_get_root_path(worktree); + cl_arg.branch_name = got_worktree_get_head_ref_name(worktree); + if (!histedit_in_progress) { + if (strncmp(cl_arg.branch_name, "refs/heads/", 11) != 0) { + error = got_error(GOT_ERR_COMMIT_BRANCH); + goto done; + } + cl_arg.branch_name += 11; + } + cl_arg.repo_path = got_repo_get_path(repo); + error = got_worktree_commit(&id, worktree, &paths, author, committer, + allow_bad_symlinks, show_diff, commit_conflicts, + collect_commit_logmsg, &cl_arg, print_status, NULL, repo); + if (error) { + if (error->code != GOT_ERR_COMMIT_MSG_EMPTY && + cl_arg.logmsg_path != NULL) + preserve_logmsg = 1; + goto done; + } + + error = got_object_id_str(&id_str, id); + if (error) + goto done; + printf("Created commit %s\n", id_str); + + TAILQ_FOREACH(re, &refs, entry) { + error = got_ref_delete(re->ref, repo); + if (error) + goto done; + } + +done: + if (preserve_logmsg) { + fprintf(stderr, "%s: log message preserved in %s\n", + getprogname(), cl_arg.logmsg_path); + } else if (cl_arg.logmsg_path && unlink(cl_arg.logmsg_path) == -1 && + error == NULL) + error = got_error_from_errno2("unlink", cl_arg.logmsg_path); + free(cl_arg.logmsg_path); + if (merged_logmsg && unlink(merged_logmsg) == -1 && error == NULL) + error = got_error_from_errno2("unlink", merged_logmsg); + free(merged_logmsg); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (worktree) + got_worktree_close(worktree); + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + got_ref_list_free(&refs); + got_pathlist_free(&paths, GOT_PATHLIST_FREE_PATH); + free(cwd); + free(id_str); + free(gitconfig_path); + free(editor); + free(committer); + free(prepared_logmsg); + return error; +} + +__dead static void +usage_send(void) +{ + fprintf(stderr, "usage: %s send [-afqTv] [-b branch] [-d branch] " + "[-r repository-path] [-t tag] [remote-repository]\n", + getprogname()); + exit(1); +} + +static void +print_load_info(int print_colored, int print_found, int print_trees, + int ncolored, int nfound, int ntrees) +{ + if (print_colored) { + printf("%d commit%s colored", ncolored, + ncolored == 1 ? "" : "s"); + } + if (print_found) { + printf("%s%d object%s found", + ncolored > 0 ? "; " : "", + nfound, nfound == 1 ? "" : "s"); + } + if (print_trees) { + printf("; %d tree%s scanned", ntrees, + ntrees == 1 ? "" : "s"); + } +} + +struct got_send_progress_arg { + char last_scaled_packsize[FMT_SCALED_STRSIZE]; + int verbosity; + int last_ncolored; + int last_nfound; + int last_ntrees; + int loading_done; + int last_ncommits; + int last_nobj_total; + int last_p_deltify; + int last_p_written; + int last_p_sent; + int printed_something; + int sent_something; + struct got_pathlist_head *delete_branches; +}; + +static const struct got_error * +send_progress(void *arg, int ncolored, int nfound, int ntrees, + off_t packfile_size, int ncommits, int nobj_total, int nobj_deltify, + int nobj_written, off_t bytes_sent, const char *refname, + const char *errmsg, int success) +{ + struct got_send_progress_arg *a = arg; + char scaled_packsize[FMT_SCALED_STRSIZE]; + char scaled_sent[FMT_SCALED_STRSIZE]; + int p_deltify = 0, p_written = 0, p_sent = 0; + int print_colored = 0, print_found = 0, print_trees = 0; + int print_searching = 0, print_total = 0; + int print_deltify = 0, print_written = 0, print_sent = 0; + + if (a->verbosity < 0) + return NULL; + + if (refname) { + const char *status = success ? "accepted" : "rejected"; + + if (success) { + struct got_pathlist_entry *pe; + TAILQ_FOREACH(pe, a->delete_branches, entry) { + const char *branchname = pe->path; + if (got_path_cmp(branchname, refname, + strlen(branchname), strlen(refname)) == 0) { + status = "deleted"; + a->sent_something = 1; + break; + } + } + } + + if (a->printed_something) + putchar('\n'); + printf("Server has %s %s", status, refname); + if (errmsg) + printf(": %s", errmsg); + a->printed_something = 1; + return NULL; + } + + if (a->last_ncolored != ncolored) { + print_colored = 1; + a->last_ncolored = ncolored; + } + + if (a->last_nfound != nfound) { + print_colored = 1; + print_found = 1; + a->last_nfound = nfound; + } + + if (a->last_ntrees != ntrees) { + print_colored = 1; + print_found = 1; + print_trees = 1; + a->last_ntrees = ntrees; + } + + if ((print_colored || print_found || print_trees) && + !a->loading_done) { + printf("\r"); + print_load_info(print_colored, print_found, print_trees, + ncolored, nfound, ntrees); + a->printed_something = 1; + fflush(stdout); + return NULL; + } else if (!a->loading_done) { + printf("\r"); + print_load_info(1, 1, 1, ncolored, nfound, ntrees); + printf("\n"); + a->loading_done = 1; + } + + if (fmt_scaled(packfile_size, scaled_packsize) == -1) + return got_error_from_errno("fmt_scaled"); + if (fmt_scaled(bytes_sent, scaled_sent) == -1) + return got_error_from_errno("fmt_scaled"); + + if (a->last_ncommits != ncommits) { + print_searching = 1; + a->last_ncommits = ncommits; + } + + if (a->last_nobj_total != nobj_total) { + print_searching = 1; + print_total = 1; + a->last_nobj_total = nobj_total; + } + + if (packfile_size > 0 && (a->last_scaled_packsize[0] == '\0' || + strcmp(scaled_packsize, a->last_scaled_packsize)) != 0) { + if (strlcpy(a->last_scaled_packsize, scaled_packsize, + FMT_SCALED_STRSIZE) >= FMT_SCALED_STRSIZE) + return got_error(GOT_ERR_NO_SPACE); + } + + if (nobj_deltify > 0 || nobj_written > 0) { + if (nobj_deltify > 0) { + p_deltify = (nobj_deltify * 100) / nobj_total; + if (p_deltify != a->last_p_deltify) { + a->last_p_deltify = p_deltify; + print_searching = 1; + print_total = 1; + print_deltify = 1; + } + } + if (nobj_written > 0) { + p_written = (nobj_written * 100) / nobj_total; + if (p_written != a->last_p_written) { + a->last_p_written = p_written; + print_searching = 1; + print_total = 1; + print_deltify = 1; + print_written = 1; + } + } + } + + if (bytes_sent > 0) { + p_sent = (bytes_sent * 100) / packfile_size; + if (p_sent != a->last_p_sent) { + a->last_p_sent = p_sent; + print_searching = 1; + print_total = 1; + print_deltify = 1; + print_written = 1; + print_sent = 1; + } + a->sent_something = 1; + } + + if (print_searching || print_total || print_deltify || print_written || + print_sent) + printf("\r"); + if (print_searching) + printf("packing %d reference%s", ncommits, + ncommits == 1 ? "" : "s"); + if (print_total) + printf("; %d object%s", nobj_total, + nobj_total == 1 ? "" : "s"); + if (print_deltify) + printf("; deltify: %d%%", p_deltify); + if (print_sent) + printf("; uploading pack: %*s %d%%", FMT_SCALED_STRSIZE - 2, + scaled_packsize, p_sent); + else if (print_written) + printf("; writing pack: %*s %d%%", FMT_SCALED_STRSIZE - 2, + scaled_packsize, p_written); + if (print_searching || print_total || print_deltify || + print_written || print_sent) { + a->printed_something = 1; + fflush(stdout); + } + return NULL; +} + +static const struct got_error * +cmd_send(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + char *cwd = NULL, *repo_path = NULL; + const char *remote_name; + char *proto = NULL, *host = NULL, *port = NULL; + char *repo_name = NULL, *server_path = NULL; + const struct got_remote_repo *remotes, *remote = NULL; + int nremotes, nbranches = 0, ndelete_branches = 0; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + const struct got_gotconfig *repo_conf = NULL, *worktree_conf = NULL; + struct got_pathlist_head branches; + struct got_pathlist_head tags; + struct got_reflist_head all_branches; + struct got_reflist_head all_tags; + struct got_pathlist_head delete_args; + struct got_pathlist_head delete_branches; + struct got_reflist_entry *re; + struct got_pathlist_entry *pe; + int i, ch, sendfd = -1, sendstatus; + pid_t sendpid = -1; + struct got_send_progress_arg spa; + int verbosity = 0, overwrite_refs = 0; + int send_all_branches = 0, send_all_tags = 0; + struct got_reference *ref = NULL; + int *pack_fds = NULL; + + TAILQ_INIT(&branches); + TAILQ_INIT(&tags); + TAILQ_INIT(&all_branches); + TAILQ_INIT(&all_tags); + TAILQ_INIT(&delete_args); + TAILQ_INIT(&delete_branches); + + while ((ch = getopt(argc, argv, "ab:d:fqr:Tt:v")) != -1) { + switch (ch) { + case 'a': + send_all_branches = 1; + break; + case 'b': + error = got_pathlist_append(&branches, optarg, NULL); + if (error) + return error; + nbranches++; + break; + case 'd': + error = got_pathlist_append(&delete_args, optarg, NULL); + if (error) + return error; + break; + case 'f': + overwrite_refs = 1; + break; + case 'q': + verbosity = -1; + break; + case 'r': + repo_path = realpath(optarg, NULL); + if (repo_path == NULL) + return got_error_from_errno2("realpath", + optarg); + got_path_strip_trailing_slashes(repo_path); + break; + case 'T': + send_all_tags = 1; + break; + case 't': + error = got_pathlist_append(&tags, optarg, NULL); + if (error) + return error; + break; + case 'v': + if (verbosity < 0) + verbosity = 0; + else if (verbosity < 3) + verbosity++; + break; + default: + usage_send(); + /* NOTREACHED */ + } + } + argc -= optind; + argv += optind; + + if (send_all_branches && !TAILQ_EMPTY(&branches)) + option_conflict('a', 'b'); + if (send_all_tags && !TAILQ_EMPTY(&tags)) + option_conflict('T', 't'); + + + if (argc == 0) + remote_name = GOT_SEND_DEFAULT_REMOTE_NAME; + else if (argc == 1) + remote_name = argv[0]; + else + usage_send(); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + if (repo_path == NULL) { + error = got_worktree_open(&worktree, cwd); + if (error && error->code != GOT_ERR_NOT_WORKTREE) + goto done; + else + error = NULL; + if (worktree) { + repo_path = + strdup(got_worktree_get_repo_path(worktree)); + if (repo_path == NULL) + error = got_error_from_errno("strdup"); + if (error) + goto done; + } else { + repo_path = strdup(cwd); + if (repo_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + } + + error = got_repo_open(&repo, repo_path, NULL, pack_fds); + if (error) + goto done; + + if (worktree) { + worktree_conf = got_worktree_get_gotconfig(worktree); + if (worktree_conf) { + got_gotconfig_get_remotes(&nremotes, &remotes, + worktree_conf); + for (i = 0; i < nremotes; i++) { + if (strcmp(remotes[i].name, remote_name) == 0) { + remote = &remotes[i]; + break; + } + } + } + } + if (remote == NULL) { + repo_conf = got_repo_get_gotconfig(repo); + if (repo_conf) { + got_gotconfig_get_remotes(&nremotes, &remotes, + repo_conf); + for (i = 0; i < nremotes; i++) { + if (strcmp(remotes[i].name, remote_name) == 0) { + remote = &remotes[i]; + break; + } + } + } + } + if (remote == NULL) { + got_repo_get_gitconfig_remotes(&nremotes, &remotes, repo); + for (i = 0; i < nremotes; i++) { + if (strcmp(remotes[i].name, remote_name) == 0) { + remote = &remotes[i]; + break; + } + } + } + if (remote == NULL) { + error = got_error_path(remote_name, GOT_ERR_NO_REMOTE); + goto done; + } + + error = got_dial_parse_uri(&proto, &host, &port, &server_path, + &repo_name, remote->send_url); + if (error) + goto done; + + if (strcmp(proto, "git") == 0) { +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec " + "sendfd dns inet unveil", NULL) == -1) + err(1, "pledge"); +#endif + } else if (strcmp(proto, "git+ssh") == 0 || + strcmp(proto, "ssh") == 0) { +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec " + "sendfd unveil", NULL) == -1) + err(1, "pledge"); +#endif + } else if (strcmp(proto, "http") == 0 || + strcmp(proto, "git+http") == 0) { + error = got_error_path(proto, GOT_ERR_NOT_IMPL); + goto done; + } else { + error = got_error_path(proto, GOT_ERR_BAD_PROTO); + goto done; + } + + error = got_dial_apply_unveil(proto); + if (error) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 0, NULL); + if (error) + goto done; + + if (send_all_branches) { + error = got_ref_list(&all_branches, repo, "refs/heads", + got_ref_cmp_by_name, NULL); + if (error) + goto done; + TAILQ_FOREACH(re, &all_branches, entry) { + const char *branchname = got_ref_get_name(re->ref); + error = got_pathlist_append(&branches, + branchname, NULL); + if (error) + goto done; + nbranches++; + } + } else if (nbranches == 0) { + for (i = 0; i < remote->nsend_branches; i++) { + error = got_pathlist_append(&branches, + remote->send_branches[i], NULL); + if (error) + goto done; + } + } + + if (send_all_tags) { + error = got_ref_list(&all_tags, repo, "refs/tags", + got_ref_cmp_by_name, NULL); + if (error) + goto done; + TAILQ_FOREACH(re, &all_tags, entry) { + const char *tagname = got_ref_get_name(re->ref); + error = got_pathlist_append(&tags, + tagname, NULL); + if (error) + goto done; + } + } + + /* + * To prevent accidents only branches in refs/heads/ can be deleted + * with 'got send -d'. + * Deleting anything else requires local repository access or Git. + */ + TAILQ_FOREACH(pe, &delete_args, entry) { + const char *branchname = pe->path; + char *s; + struct got_pathlist_entry *new; + if (strncmp(branchname, "refs/heads/", 11) == 0) { + s = strdup(branchname); + if (s == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else { + if (asprintf(&s, "refs/heads/%s", branchname) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + } + error = got_pathlist_insert(&new, &delete_branches, s, NULL); + if (error || new == NULL /* duplicate */) + free(s); + if (error) + goto done; + ndelete_branches++; + } + + if (nbranches == 0 && ndelete_branches == 0) { + struct got_reference *head_ref; + if (worktree) + error = got_ref_open(&head_ref, repo, + got_worktree_get_head_ref_name(worktree), 0); + else + error = got_ref_open(&head_ref, repo, GOT_REF_HEAD, 0); + if (error) + goto done; + if (got_ref_is_symbolic(head_ref)) { + error = got_ref_resolve_symbolic(&ref, repo, head_ref); + got_ref_close(head_ref); + if (error) + goto done; + } else + ref = head_ref; + error = got_pathlist_append(&branches, got_ref_get_name(ref), + NULL); + if (error) + goto done; + nbranches++; + } + + if (verbosity >= 0) { + printf("Connecting to \"%s\" %s://%s%s%s%s%s\n", + remote->name, proto, host, + port ? ":" : "", port ? port : "", + *server_path == '/' ? "" : "/", server_path); + } + + error = got_send_connect(&sendpid, &sendfd, proto, host, port, + server_path, verbosity); + if (error) + goto done; + + memset(&spa, 0, sizeof(spa)); + spa.last_scaled_packsize[0] = '\0'; + spa.last_p_deltify = -1; + spa.last_p_written = -1; + spa.verbosity = verbosity; + spa.delete_branches = &delete_branches; + error = got_send_pack(remote_name, &branches, &tags, &delete_branches, + verbosity, overwrite_refs, sendfd, repo, send_progress, &spa, + check_cancelled, NULL); + if (spa.printed_something) + putchar('\n'); + if (error) + goto done; + if (!spa.sent_something && verbosity >= 0) + printf("Already up-to-date\n"); +done: + if (sendpid > 0) { + if (kill(sendpid, SIGTERM) == -1) + error = got_error_from_errno("kill"); + if (waitpid(sendpid, &sendstatus, 0) == -1 && error == NULL) + error = got_error_from_errno("waitpid"); + } + if (sendfd != -1 && close(sendfd) == -1 && error == NULL) + error = got_error_from_errno("close"); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (worktree) + got_worktree_close(worktree); + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + if (ref) + got_ref_close(ref); + got_pathlist_free(&branches, GOT_PATHLIST_FREE_NONE); + got_pathlist_free(&tags, GOT_PATHLIST_FREE_NONE); + got_ref_list_free(&all_branches); + got_ref_list_free(&all_tags); + got_pathlist_free(&delete_args, GOT_PATHLIST_FREE_NONE); + got_pathlist_free(&delete_branches, GOT_PATHLIST_FREE_PATH); + free(cwd); + free(repo_path); + free(proto); + free(host); + free(port); + free(server_path); + free(repo_name); + return error; +} + +/* + * Print and if delete is set delete all ref_prefix references. + * If wanted_ref is not NULL, only print or delete this reference. + */ +static const struct got_error * +process_logmsg_refs(const char *ref_prefix, size_t prefix_len, + const char *wanted_ref, int delete, struct got_worktree *worktree, + struct got_repository *repo) +{ + const struct got_error *err; + struct got_pathlist_head paths; + struct got_reflist_head refs; + struct got_reflist_entry *re; + struct got_reflist_object_id_map *refs_idmap = NULL; + struct got_commit_object *commit = NULL; + struct got_object_id *id = NULL; + const char *header_prefix; + char *uuidstr = NULL; + int found = 0; + + TAILQ_INIT(&refs); + TAILQ_INIT(&paths); + + err = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, repo); + if (err) + goto done; + + err = got_reflist_object_id_map_create(&refs_idmap, &refs, repo); + if (err) + goto done; + + if (worktree != NULL) { + err = got_worktree_get_uuid(&uuidstr, worktree); + if (err) + goto done; + } + + if (wanted_ref) { + if (strncmp(wanted_ref, "refs/heads/", 11) == 0) + wanted_ref += 11; + } + + if (strcmp(ref_prefix, GOT_WORKTREE_BACKOUT_REF_PREFIX) == 0) + header_prefix = "backout"; + else + header_prefix = "cherrypick"; + + TAILQ_FOREACH(re, &refs, entry) { + const char *refname, *wt; + + refname = got_ref_get_name(re->ref); + + err = check_cancelled(NULL); + if (err) + goto done; + + if (strncmp(refname, ref_prefix, prefix_len) == 0) + refname += prefix_len + 1; /* skip '-' delimiter */ + else + continue; + + wt = refname; + + if (worktree == NULL || strncmp(refname, uuidstr, + GOT_WORKTREE_UUID_STRLEN) == 0) + refname += GOT_WORKTREE_UUID_STRLEN + 1; /* skip '-' */ + else + continue; + + err = got_repo_match_object_id(&id, NULL, refname, + GOT_OBJ_TYPE_COMMIT, NULL, repo); + if (err) + goto done; + + err = got_object_open_as_commit(&commit, repo, id); + if (err) + goto done; + + if (wanted_ref) + found = strncmp(wanted_ref, refname, + strlen(wanted_ref)) == 0; + if (wanted_ref && !found) { + struct got_reflist_head *ci_refs; + + ci_refs = got_reflist_object_id_map_lookup(refs_idmap, + id); + + if (ci_refs) { + char *refs_str = NULL; + char const *r = NULL; + + err = build_refs_str(&refs_str, ci_refs, id, + repo, 1); + if (err) + goto done; + + r = refs_str; + while (r) { + if (strncmp(r, wanted_ref, + strlen(wanted_ref)) == 0) { + found = 1; + break; + } + r = strchr(r, ' '); + if (r) + ++r; + } + free(refs_str); + } + } + + if (wanted_ref == NULL || found) { + if (delete) { + err = got_ref_delete(re->ref, repo); + if (err) + goto done; + printf("Deleted: "); + err = print_commit_oneline(commit, id, repo, + refs_idmap); + } else { + /* + * Print paths modified by commit to help + * associate commits with worktree changes. + */ + err = get_changed_paths(&paths, commit, + repo, NULL); + if (err) + goto done; + + err = print_commit(commit, id, repo, NULL, + &paths, NULL, 0, 0, refs_idmap, NULL, + header_prefix); + got_pathlist_free(&paths, + GOT_PATHLIST_FREE_ALL); + + if (worktree == NULL) + printf("work tree: %.*s\n\n", + GOT_WORKTREE_UUID_STRLEN, wt); + } + if (err || found) + goto done; + } + + got_object_commit_close(commit); + commit = NULL; + free(id); + id = NULL; + } + + if (wanted_ref != NULL && !found) + err = got_error_fmt(GOT_ERR_NOT_REF, "%s", wanted_ref); + +done: + free(id); + free(uuidstr); + got_ref_list_free(&refs); + got_pathlist_free(&paths, GOT_PATHLIST_FREE_ALL); + if (refs_idmap) + got_reflist_object_id_map_free(refs_idmap); + if (commit) + got_object_commit_close(commit); + return err; +} + +/* + * Create new temp "logmsg" ref of the backed-out or cherrypicked commit + * identified by id for log messages to prepopulate the editor on commit. + */ +static const struct got_error * +logmsg_ref(struct got_object_id *id, const char *prefix, + struct got_worktree *worktree, struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *idstr, *ref = NULL, *refname = NULL; + int histedit_in_progress; + int rebase_in_progress, merge_in_progress; + + /* + * Silenty refuse to create merge reference if any histedit, merge, + * or rebase operation is in progress. + */ + err = got_worktree_histedit_in_progress(&histedit_in_progress, + worktree); + if (err) + return err; + if (histedit_in_progress) + return NULL; + + err = got_worktree_rebase_in_progress(&rebase_in_progress, worktree); + if (err) + return err; + if (rebase_in_progress) + return NULL; + + err = got_worktree_merge_in_progress(&merge_in_progress, worktree, + repo); + if (err) + return err; + if (merge_in_progress) + return NULL; + + err = got_object_id_str(&idstr, id); + if (err) + return err; + + err = got_worktree_get_logmsg_ref_name(&refname, worktree, prefix); + if (err) + goto done; + + if (asprintf(&ref, "%s-%s", refname, idstr) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + + err = create_ref(ref, got_worktree_get_base_commit_id(worktree), + -1, repo); +done: + free(ref); + free(idstr); + free(refname); + return err; +} + +__dead static void +usage_cherrypick(void) +{ + fprintf(stderr, "usage: %s cherrypick [-lX] [commit-id]\n", + getprogname()); + exit(1); +} + +static const struct got_error * +cmd_cherrypick(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_worktree *worktree = NULL; + struct got_repository *repo = NULL; + char *cwd = NULL, *commit_id_str = NULL; + struct got_object_id *commit_id = NULL; + struct got_commit_object *commit = NULL; + struct got_object_qid *pid; + int ch, list_refs = 0, remove_refs = 0; + struct got_update_progress_arg upa; + int *pack_fds = NULL; + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "lX")) != -1) { + switch (ch) { + case 'l': + list_refs = 1; + break; + case 'X': + remove_refs = 1; + break; + default: + usage_cherrypick(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (list_refs || remove_refs) { + if (argc != 0 && argc != 1) + usage_cherrypick(); + } else if (argc != 1) + usage_cherrypick(); + if (list_refs && remove_refs) + option_conflict('l', 'X'); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_open(&worktree, cwd); + if (error) { + if (list_refs || remove_refs) { + if (error->code != GOT_ERR_NOT_WORKTREE) + goto done; + } else { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, + "cherrypick", cwd); + goto done; + } + } + + error = got_repo_open(&repo, + worktree ? got_worktree_get_repo_path(worktree) : cwd, + NULL, pack_fds); + if (error != NULL) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 0, + worktree ? got_worktree_get_root_path(worktree) : NULL); + if (error) + goto done; + + if (list_refs || remove_refs) { + error = process_logmsg_refs(GOT_WORKTREE_CHERRYPICK_REF_PREFIX, + GOT_WORKTREE_CHERRYPICK_REF_PREFIX_LEN, + argc == 1 ? argv[0] : NULL, remove_refs, worktree, repo); + goto done; + } + + error = got_repo_match_object_id(&commit_id, NULL, argv[0], + GOT_OBJ_TYPE_COMMIT, NULL, repo); + if (error) + goto done; + error = got_object_id_str(&commit_id_str, commit_id); + if (error) + goto done; + + error = got_object_open_as_commit(&commit, repo, commit_id); + if (error) + goto done; + pid = STAILQ_FIRST(got_object_commit_get_parent_ids(commit)); + memset(&upa, 0, sizeof(upa)); + error = got_worktree_merge_files(worktree, pid ? &pid->id : NULL, + commit_id, repo, update_progress, &upa, check_cancelled, + NULL); + if (error != NULL) + goto done; + + if (upa.did_something) { + error = logmsg_ref(commit_id, + GOT_WORKTREE_CHERRYPICK_REF_PREFIX, worktree, repo); + if (error) + goto done; + printf("Merged commit %s\n", commit_id_str); + } + print_merge_progress_stats(&upa); +done: + free(cwd); + if (commit) + got_object_commit_close(commit); + free(commit_id_str); + if (worktree) + got_worktree_close(worktree); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + + return error; +} + +__dead static void +usage_backout(void) +{ + fprintf(stderr, "usage: %s backout [-lX] [commit-id]\n", getprogname()); + exit(1); +} + +static const struct got_error * +cmd_backout(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_worktree *worktree = NULL; + struct got_repository *repo = NULL; + char *cwd = NULL, *commit_id_str = NULL; + struct got_object_id *commit_id = NULL; + struct got_commit_object *commit = NULL; + struct got_object_qid *pid; + int ch, list_refs = 0, remove_refs = 0; + struct got_update_progress_arg upa; + int *pack_fds = NULL; + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "lX")) != -1) { + switch (ch) { + case 'l': + list_refs = 1; + break; + case 'X': + remove_refs = 1; + break; + default: + usage_backout(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (list_refs || remove_refs) { + if (argc != 0 && argc != 1) + usage_backout(); + } else if (argc != 1) + usage_backout(); + if (list_refs && remove_refs) + option_conflict('l', 'X'); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_open(&worktree, cwd); + if (error) { + if (list_refs || remove_refs) { + if (error->code != GOT_ERR_NOT_WORKTREE) + goto done; + } else { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, + "backout", cwd); + goto done; + } + } + + error = got_repo_open(&repo, + worktree ? got_worktree_get_repo_path(worktree) : cwd, + NULL, pack_fds); + if (error != NULL) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 0, + worktree ? got_worktree_get_root_path(worktree) : NULL); + if (error) + goto done; + + if (list_refs || remove_refs) { + error = process_logmsg_refs(GOT_WORKTREE_BACKOUT_REF_PREFIX, + GOT_WORKTREE_BACKOUT_REF_PREFIX_LEN, + argc == 1 ? argv[0] : NULL, remove_refs, worktree, repo); + goto done; + } + + error = got_repo_match_object_id(&commit_id, NULL, argv[0], + GOT_OBJ_TYPE_COMMIT, NULL, repo); + if (error) + goto done; + error = got_object_id_str(&commit_id_str, commit_id); + if (error) + goto done; + + error = got_object_open_as_commit(&commit, repo, commit_id); + if (error) + goto done; + pid = STAILQ_FIRST(got_object_commit_get_parent_ids(commit)); + if (pid == NULL) { + error = got_error(GOT_ERR_ROOT_COMMIT); + goto done; + } + + memset(&upa, 0, sizeof(upa)); + error = got_worktree_merge_files(worktree, commit_id, &pid->id, + repo, update_progress, &upa, check_cancelled, NULL); + if (error != NULL) + goto done; + + if (upa.did_something) { + error = logmsg_ref(commit_id, GOT_WORKTREE_BACKOUT_REF_PREFIX, + worktree, repo); + if (error) + goto done; + printf("Backed out commit %s\n", commit_id_str); + } + print_merge_progress_stats(&upa); +done: + free(cwd); + if (commit) + got_object_commit_close(commit); + free(commit_id_str); + if (worktree) + got_worktree_close(worktree); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + return error; +} + +__dead static void +usage_rebase(void) +{ + fprintf(stderr, "usage: %s rebase [-aCclX] [branch]\n", getprogname()); + exit(1); +} + +static void +trim_logmsg(char *logmsg, int limit) +{ + char *nl; + size_t len; + + len = strlen(logmsg); + if (len > limit) + len = limit; + logmsg[len] = '\0'; + nl = strchr(logmsg, '\n'); + if (nl) + *nl = '\0'; +} + +static const struct got_error * +get_short_logmsg(char **logmsg, int limit, struct got_commit_object *commit) +{ + const struct got_error *err; + char *logmsg0 = NULL; + const char *s; + + err = got_object_commit_get_logmsg(&logmsg0, commit); + if (err) + return err; + + s = logmsg0; + while (isspace((unsigned char)s[0])) + s++; + + *logmsg = strdup(s); + if (*logmsg == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + + trim_logmsg(*logmsg, limit); +done: + free(logmsg0); + return err; +} + +static const struct got_error * +show_rebase_merge_conflict(struct got_object_id *id, + struct got_repository *repo) +{ + const struct got_error *err; + struct got_commit_object *commit = NULL; + char *id_str = NULL, *logmsg = NULL; + + err = got_object_open_as_commit(&commit, repo, id); + if (err) + return err; + + err = got_object_id_str(&id_str, id); + if (err) + goto done; + + id_str[12] = '\0'; + + err = get_short_logmsg(&logmsg, 42, commit); + if (err) + goto done; + + printf("%s -> merge conflict: %s\n", id_str, logmsg); +done: + free(id_str); + got_object_commit_close(commit); + free(logmsg); + return err; +} + +static const struct got_error * +show_rebase_progress(struct got_commit_object *commit, + struct got_object_id *old_id, struct got_object_id *new_id) +{ + const struct got_error *err; + char *old_id_str = NULL, *new_id_str = NULL, *logmsg = NULL; + + err = got_object_id_str(&old_id_str, old_id); + if (err) + goto done; + + if (new_id) { + err = got_object_id_str(&new_id_str, new_id); + if (err) + goto done; + } + + old_id_str[12] = '\0'; + if (new_id_str) + new_id_str[12] = '\0'; + + err = get_short_logmsg(&logmsg, 42, commit); + if (err) + goto done; + + printf("%s -> %s: %s\n", old_id_str, + new_id_str ? new_id_str : "no-op change", logmsg); +done: + free(old_id_str); + free(new_id_str); + free(logmsg); + return err; +} + +static const struct got_error * +rebase_complete(struct got_worktree *worktree, struct got_fileindex *fileindex, + struct got_reference *branch, struct got_reference *tmp_branch, + struct got_repository *repo, int create_backup) +{ + printf("Switching work tree to %s\n", got_ref_get_name(branch)); + return got_worktree_rebase_complete(worktree, fileindex, + tmp_branch, branch, repo, create_backup); +} + +static const struct got_error * +rebase_commit(struct got_pathlist_head *merged_paths, + struct got_worktree *worktree, struct got_fileindex *fileindex, + struct got_reference *tmp_branch, const char *committer, + struct got_object_id *commit_id, int allow_conflict, + struct got_repository *repo) +{ + const struct got_error *error; + struct got_commit_object *commit; + struct got_object_id *new_commit_id; + + error = got_object_open_as_commit(&commit, repo, commit_id); + if (error) + return error; + + error = got_worktree_rebase_commit(&new_commit_id, merged_paths, + worktree, fileindex, tmp_branch, committer, commit, commit_id, + allow_conflict, repo); + if (error) { + if (error->code != GOT_ERR_COMMIT_NO_CHANGES) + goto done; + error = show_rebase_progress(commit, commit_id, NULL); + } else { + error = show_rebase_progress(commit, commit_id, new_commit_id); + free(new_commit_id); + } +done: + got_object_commit_close(commit); + return error; +} + +struct check_path_prefix_arg { + const char *path_prefix; + size_t len; + int errcode; +}; + +static const struct got_error * +check_path_prefix_in_diff(void *arg, struct got_blob_object *blob1, + struct got_blob_object *blob2, FILE *f1, FILE *f2, + struct got_object_id *id1, struct got_object_id *id2, + const char *path1, const char *path2, + mode_t mode1, mode_t mode2, struct got_repository *repo) +{ + struct check_path_prefix_arg *a = arg; + + if ((path1 && !got_path_is_child(path1, a->path_prefix, a->len)) || + (path2 && !got_path_is_child(path2, a->path_prefix, a->len))) + return got_error(a->errcode); + + return NULL; +} + +static const struct got_error * +check_path_prefix(struct got_object_id *parent_id, + struct got_object_id *commit_id, const char *path_prefix, + int errcode, struct got_repository *repo) +{ + const struct got_error *err; + struct got_tree_object *tree1 = NULL, *tree2 = NULL; + struct got_commit_object *commit = NULL, *parent_commit = NULL; + struct check_path_prefix_arg cpp_arg; + + if (got_path_is_root_dir(path_prefix)) + return NULL; + + err = got_object_open_as_commit(&commit, repo, commit_id); + if (err) + goto done; + + err = got_object_open_as_commit(&parent_commit, repo, parent_id); + if (err) + goto done; + + err = got_object_open_as_tree(&tree1, repo, + got_object_commit_get_tree_id(parent_commit)); + if (err) + goto done; + + err = got_object_open_as_tree(&tree2, repo, + got_object_commit_get_tree_id(commit)); + if (err) + goto done; + + cpp_arg.path_prefix = path_prefix; + while (cpp_arg.path_prefix[0] == '/') + cpp_arg.path_prefix++; + cpp_arg.len = strlen(cpp_arg.path_prefix); + cpp_arg.errcode = errcode; + err = got_diff_tree(tree1, tree2, NULL, NULL, -1, -1, "", "", repo, + check_path_prefix_in_diff, &cpp_arg, 0); +done: + if (tree1) + got_object_tree_close(tree1); + if (tree2) + got_object_tree_close(tree2); + if (commit) + got_object_commit_close(commit); + if (parent_commit) + got_object_commit_close(parent_commit); + return err; +} + +static const struct got_error * +collect_commits(struct got_object_id_queue *commits, + struct got_object_id *initial_commit_id, + struct got_object_id *iter_start_id, struct got_object_id *iter_stop_id, + const char *path_prefix, int path_prefix_errcode, + struct got_repository *repo) +{ + const struct got_error *err = NULL; + struct got_commit_graph *graph = NULL; + struct got_object_id parent_id, commit_id; + struct got_object_qid *qid; + + err = got_commit_graph_open(&graph, "/", 1); + if (err) + return err; + + err = got_commit_graph_iter_start(graph, iter_start_id, repo, + check_cancelled, NULL); + if (err) + goto done; + + memcpy(&commit_id, initial_commit_id, sizeof(commit_id)); + while (got_object_id_cmp(&commit_id, iter_stop_id) != 0) { + err = got_commit_graph_iter_next(&parent_id, graph, repo, + check_cancelled, NULL); + if (err) { + if (err->code == GOT_ERR_ITER_COMPLETED) { + err = got_error_msg(GOT_ERR_ANCESTRY, + "ran out of commits to rebase before " + "youngest common ancestor commit has " + "been reached?!?"); + } + goto done; + } else { + err = check_path_prefix(&parent_id, &commit_id, + path_prefix, path_prefix_errcode, repo); + if (err) + goto done; + + err = got_object_qid_alloc(&qid, &commit_id); + if (err) + goto done; + STAILQ_INSERT_HEAD(commits, qid, entry); + + memcpy(&commit_id, &parent_id, sizeof(commit_id)); + } + } +done: + got_commit_graph_close(graph); + return err; +} + +static const struct got_error * +get_commit_brief_str(char **brief_str, struct got_commit_object *commit) +{ + const struct got_error *err = NULL; + time_t committer_time; + struct tm tm; + char datebuf[11]; /* YYYY-MM-DD + NUL */ + char *author0 = NULL, *author, *smallerthan; + char *logmsg0 = NULL, *logmsg, *newline; + + committer_time = got_object_commit_get_committer_time(commit); + if (gmtime_r(&committer_time, &tm) == NULL) + return got_error_from_errno("gmtime_r"); + if (strftime(datebuf, sizeof(datebuf), "%G-%m-%d", &tm) == 0) + return got_error(GOT_ERR_NO_SPACE); + + author0 = strdup(got_object_commit_get_author(commit)); + if (author0 == NULL) + return got_error_from_errno("strdup"); + author = author0; + smallerthan = strchr(author, '<'); + if (smallerthan && smallerthan[1] != '\0') + author = smallerthan + 1; + author[strcspn(author, "@>")] = '\0'; + + err = got_object_commit_get_logmsg(&logmsg0, commit); + if (err) + goto done; + logmsg = logmsg0; + while (*logmsg == '\n') + logmsg++; + newline = strchr(logmsg, '\n'); + if (newline) + *newline = '\0'; + + if (asprintf(brief_str, "%s %s %s", + datebuf, author, logmsg) == -1) + err = got_error_from_errno("asprintf"); +done: + free(author0); + free(logmsg0); + return err; +} + +static const struct got_error * +delete_backup_ref(struct got_reference *ref, struct got_object_id *id, + struct got_repository *repo) +{ + const struct got_error *err; + char *id_str; + + err = got_object_id_str(&id_str, id); + if (err) + return err; + + err = got_ref_delete(ref, repo); + if (err) + goto done; + + printf("Deleted %s: %s\n", got_ref_get_name(ref), id_str); +done: + free(id_str); + return err; +} + +static const struct got_error * +print_backup_ref(const char *branch_name, const char *new_id_str, + struct got_object_id *old_commit_id, struct got_commit_object *old_commit, + struct got_reflist_object_id_map *refs_idmap, + struct got_repository *repo) +{ + const struct got_error *err = NULL; + struct got_reflist_head *refs; + char *refs_str = NULL; + struct got_object_id *new_commit_id = NULL; + struct got_commit_object *new_commit = NULL; + char *new_commit_brief_str = NULL; + struct got_object_id *yca_id = NULL; + struct got_commit_object *yca_commit = NULL; + char *yca_id_str = NULL, *yca_brief_str = NULL; + char *custom_refs_str; + + if (asprintf(&custom_refs_str, "formerly %s", branch_name) == -1) + return got_error_from_errno("asprintf"); + + err = print_commit(old_commit, old_commit_id, repo, NULL, NULL, NULL, + 0, 0, refs_idmap, custom_refs_str, NULL); + if (err) + goto done; + + err = got_object_resolve_id_str(&new_commit_id, repo, new_id_str); + if (err) + goto done; + + refs = got_reflist_object_id_map_lookup(refs_idmap, new_commit_id); + if (refs) { + err = build_refs_str(&refs_str, refs, new_commit_id, repo, 0); + if (err) + goto done; + } + + err = got_object_open_as_commit(&new_commit, repo, new_commit_id); + if (err) + goto done; + + err = get_commit_brief_str(&new_commit_brief_str, new_commit); + if (err) + goto done; + + err = got_commit_graph_find_youngest_common_ancestor(&yca_id, + old_commit_id, new_commit_id, 1, repo, check_cancelled, NULL); + if (err) + goto done; + + printf("has become commit %s%s%s%s\n %s\n", new_id_str, + refs_str ? " (" : "", refs_str ? refs_str : "", + refs_str ? ")" : "", new_commit_brief_str); + if (yca_id && got_object_id_cmp(yca_id, new_commit_id) != 0 && + got_object_id_cmp(yca_id, old_commit_id) != 0) { + free(refs_str); + refs_str = NULL; + + err = got_object_open_as_commit(&yca_commit, repo, yca_id); + if (err) + goto done; + + err = get_commit_brief_str(&yca_brief_str, yca_commit); + if (err) + goto done; + + err = got_object_id_str(&yca_id_str, yca_id); + if (err) + goto done; + + refs = got_reflist_object_id_map_lookup(refs_idmap, yca_id); + if (refs) { + err = build_refs_str(&refs_str, refs, yca_id, repo, 0); + if (err) + goto done; + } + printf("history forked at %s%s%s%s\n %s\n", + yca_id_str, + refs_str ? " (" : "", refs_str ? refs_str : "", + refs_str ? ")" : "", yca_brief_str); + } +done: + free(custom_refs_str); + free(new_commit_id); + free(refs_str); + free(yca_id); + free(yca_id_str); + free(yca_brief_str); + if (new_commit) + got_object_commit_close(new_commit); + if (yca_commit) + got_object_commit_close(yca_commit); + + return err; +} + +static const struct got_error * +worktree_has_logmsg_ref(const char *caller, struct got_worktree *worktree, + struct got_repository *repo) +{ + const struct got_error *err; + struct got_reflist_head refs; + struct got_reflist_entry *re; + char *uuidstr = NULL; + static char msg[160]; + + TAILQ_INIT(&refs); + + err = got_worktree_get_uuid(&uuidstr, worktree); + if (err) + goto done; + + err = got_ref_list(&refs, repo, "refs/got/worktree", + got_ref_cmp_by_name, repo); + if (err) + goto done; + + TAILQ_FOREACH(re, &refs, entry) { + const char *cmd, *refname, *type; + + refname = got_ref_get_name(re->ref); + + if (strncmp(refname, GOT_WORKTREE_CHERRYPICK_REF_PREFIX, + GOT_WORKTREE_CHERRYPICK_REF_PREFIX_LEN) == 0) { + refname += GOT_WORKTREE_CHERRYPICK_REF_PREFIX_LEN + 1; + cmd = "cherrypick"; + type = "cherrypicked"; + } else if (strncmp(refname, GOT_WORKTREE_BACKOUT_REF_PREFIX, + GOT_WORKTREE_BACKOUT_REF_PREFIX_LEN) == 0) { + refname += GOT_WORKTREE_BACKOUT_REF_PREFIX_LEN + 1; + cmd = "backout"; + type = "backed-out"; + } else + continue; + + if (strncmp(refname, uuidstr, GOT_WORKTREE_UUID_STRLEN) != 0) + continue; + + snprintf(msg, sizeof(msg), + "work tree has references created by %s commits which " + "must be removed with 'got %s -X' before running the %s " + "command", type, cmd, caller); + err = got_error_msg(GOT_ERR_WORKTREE_META, msg); + goto done; + } + +done: + free(uuidstr); + got_ref_list_free(&refs); + return err; +} + +static const struct got_error * +process_backup_refs(const char *backup_ref_prefix, + const char *wanted_branch_name, + int delete, struct got_repository *repo) +{ + const struct got_error *err; + struct got_reflist_head refs, backup_refs; + struct got_reflist_entry *re; + const size_t backup_ref_prefix_len = strlen(backup_ref_prefix); + struct got_object_id *old_commit_id = NULL; + char *branch_name = NULL; + struct got_commit_object *old_commit = NULL; + struct got_reflist_object_id_map *refs_idmap = NULL; + int wanted_branch_found = 0; + + TAILQ_INIT(&refs); + TAILQ_INIT(&backup_refs); + + err = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, NULL); + if (err) + return err; + + err = got_reflist_object_id_map_create(&refs_idmap, &refs, repo); + if (err) + goto done; + + if (wanted_branch_name) { + if (strncmp(wanted_branch_name, "refs/heads/", 11) == 0) + wanted_branch_name += 11; + } + + err = got_ref_list(&backup_refs, repo, backup_ref_prefix, + got_ref_cmp_by_commit_timestamp_descending, repo); + if (err) + goto done; + + TAILQ_FOREACH(re, &backup_refs, entry) { + const char *refname = got_ref_get_name(re->ref); + char *slash; + + err = check_cancelled(NULL); + if (err) + break; + + err = got_ref_resolve(&old_commit_id, repo, re->ref); + if (err) + break; + + err = got_object_open_as_commit(&old_commit, repo, + old_commit_id); + if (err) + break; + + if (strncmp(backup_ref_prefix, refname, + backup_ref_prefix_len) == 0) + refname += backup_ref_prefix_len; + + while (refname[0] == '/') + refname++; + + branch_name = strdup(refname); + if (branch_name == NULL) { + err = got_error_from_errno("strdup"); + break; + } + slash = strrchr(branch_name, '/'); + if (slash) { + *slash = '\0'; + refname += strlen(branch_name) + 1; + } + + if (wanted_branch_name == NULL || + strcmp(wanted_branch_name, branch_name) == 0) { + wanted_branch_found = 1; + if (delete) { + err = delete_backup_ref(re->ref, + old_commit_id, repo); + } else { + err = print_backup_ref(branch_name, refname, + old_commit_id, old_commit, refs_idmap, + repo); + } + if (err) + break; + } + + free(old_commit_id); + old_commit_id = NULL; + free(branch_name); + branch_name = NULL; + got_object_commit_close(old_commit); + old_commit = NULL; + } + + if (wanted_branch_name && !wanted_branch_found) { + err = got_error_fmt(GOT_ERR_NOT_REF, + "%s/%s/", backup_ref_prefix, wanted_branch_name); + } +done: + if (refs_idmap) + got_reflist_object_id_map_free(refs_idmap); + got_ref_list_free(&refs); + got_ref_list_free(&backup_refs); + free(old_commit_id); + free(branch_name); + if (old_commit) + got_object_commit_close(old_commit); + return err; +} + +static const struct got_error * +abort_progress(void *arg, unsigned char status, const char *path) +{ + /* + * Unversioned files should not clutter progress output when + * an operation is aborted. + */ + if (status == GOT_STATUS_UNVERSIONED) + return NULL; + + return update_progress(arg, status, path); +} + +static const struct got_error * +cmd_rebase(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_worktree *worktree = NULL; + struct got_repository *repo = NULL; + struct got_fileindex *fileindex = NULL; + char *cwd = NULL, *committer = NULL, *gitconfig_path = NULL; + struct got_reference *branch = NULL; + struct got_reference *new_base_branch = NULL, *tmp_branch = NULL; + struct got_object_id *commit_id = NULL, *parent_id = NULL; + struct got_object_id *resume_commit_id = NULL; + struct got_object_id *branch_head_commit_id = NULL, *yca_id = NULL; + struct got_object_id *head_commit_id = NULL; + struct got_reference *head_ref = NULL; + struct got_commit_object *commit = NULL; + int ch, rebase_in_progress = 0, abort_rebase = 0, continue_rebase = 0; + int histedit_in_progress = 0, merge_in_progress = 0; + int create_backup = 1, list_backups = 0, delete_backups = 0; + int allow_conflict = 0; + struct got_object_id_queue commits; + struct got_pathlist_head merged_paths; + const struct got_object_id_queue *parent_ids; + struct got_object_qid *qid, *pid; + struct got_update_progress_arg upa; + int *pack_fds = NULL; + + STAILQ_INIT(&commits); + TAILQ_INIT(&merged_paths); + memset(&upa, 0, sizeof(upa)); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "aCclX")) != -1) { + switch (ch) { + case 'a': + abort_rebase = 1; + break; + case 'C': + allow_conflict = 1; + break; + case 'c': + continue_rebase = 1; + break; + case 'l': + list_backups = 1; + break; + case 'X': + delete_backups = 1; + break; + default: + usage_rebase(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (list_backups) { + if (abort_rebase) + option_conflict('l', 'a'); + if (allow_conflict) + option_conflict('l', 'C'); + if (continue_rebase) + option_conflict('l', 'c'); + if (delete_backups) + option_conflict('l', 'X'); + if (argc != 0 && argc != 1) + usage_rebase(); + } else if (delete_backups) { + if (abort_rebase) + option_conflict('X', 'a'); + if (allow_conflict) + option_conflict('X', 'C'); + if (continue_rebase) + option_conflict('X', 'c'); + if (list_backups) + option_conflict('l', 'X'); + if (argc != 0 && argc != 1) + usage_rebase(); + } else if (allow_conflict) { + if (abort_rebase) + option_conflict('C', 'a'); + if (!continue_rebase) + errx(1, "-C option requires -c"); + } else { + if (abort_rebase && continue_rebase) + usage_rebase(); + else if (abort_rebase || continue_rebase) { + if (argc != 0) + usage_rebase(); + } else if (argc != 1) + usage_rebase(); + } + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_open(&worktree, cwd); + if (error) { + if (list_backups || delete_backups) { + if (error->code != GOT_ERR_NOT_WORKTREE) + goto done; + } else { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, + "rebase", cwd); + goto done; + } + } + + error = get_gitconfig_path(&gitconfig_path); + if (error) + goto done; + error = got_repo_open(&repo, + worktree ? got_worktree_get_repo_path(worktree) : cwd, + gitconfig_path, pack_fds); + if (error != NULL) + goto done; + + if (worktree != NULL && !list_backups && !delete_backups) { + error = worktree_has_logmsg_ref("rebase", worktree, repo); + if (error) + goto done; + } + + error = get_author(&committer, repo, worktree); + if (error && error->code != GOT_ERR_COMMIT_NO_AUTHOR) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 0, + worktree ? got_worktree_get_root_path(worktree) : NULL); + if (error) + goto done; + + if (list_backups || delete_backups) { + error = process_backup_refs( + GOT_WORKTREE_REBASE_BACKUP_REF_PREFIX, + argc == 1 ? argv[0] : NULL, delete_backups, repo); + goto done; /* nothing else to do */ + } + + error = got_worktree_histedit_in_progress(&histedit_in_progress, + worktree); + if (error) + goto done; + if (histedit_in_progress) { + error = got_error(GOT_ERR_HISTEDIT_BUSY); + goto done; + } + + error = got_worktree_merge_in_progress(&merge_in_progress, + worktree, repo); + if (error) + goto done; + if (merge_in_progress) { + error = got_error(GOT_ERR_MERGE_BUSY); + goto done; + } + + error = got_worktree_rebase_in_progress(&rebase_in_progress, worktree); + if (error) + goto done; + + if (abort_rebase) { + if (!rebase_in_progress) { + error = got_error(GOT_ERR_NOT_REBASING); + goto done; + } + error = got_worktree_rebase_continue(&resume_commit_id, + &new_base_branch, &tmp_branch, &branch, &fileindex, + worktree, repo); + if (error) + goto done; + printf("Switching work tree to %s\n", + got_ref_get_symref_target(new_base_branch)); + error = got_worktree_rebase_abort(worktree, fileindex, repo, + new_base_branch, abort_progress, &upa); + if (error) + goto done; + printf("Rebase of %s aborted\n", got_ref_get_name(branch)); + print_merge_progress_stats(&upa); + goto done; /* nothing else to do */ + } + + if (continue_rebase) { + if (!rebase_in_progress) { + error = got_error(GOT_ERR_NOT_REBASING); + goto done; + } + error = got_worktree_rebase_continue(&resume_commit_id, + &new_base_branch, &tmp_branch, &branch, &fileindex, + worktree, repo); + if (error) + goto done; + + error = rebase_commit(NULL, worktree, fileindex, tmp_branch, + committer, resume_commit_id, allow_conflict, repo); + if (error) + goto done; + + yca_id = got_object_id_dup(resume_commit_id); + if (yca_id == NULL) { + error = got_error_from_errno("got_object_id_dup"); + goto done; + } + } else { + error = got_ref_open(&branch, repo, argv[0], 0); + if (error != NULL) + goto done; + if (strncmp(got_ref_get_name(branch), "refs/heads/", 11) != 0) { + error = got_error_msg(GOT_ERR_COMMIT_BRANCH, + "will not rebase a branch which lives outside " + "the \"refs/heads/\" reference namespace"); + goto done; + } + } + + error = got_ref_resolve(&branch_head_commit_id, repo, branch); + if (error) + goto done; + + if (!continue_rebase) { + struct got_object_id *base_commit_id; + + error = got_ref_open(&head_ref, repo, + got_worktree_get_head_ref_name(worktree), 0); + if (error) + goto done; + error = got_ref_resolve(&head_commit_id, repo, head_ref); + if (error) + goto done; + base_commit_id = got_worktree_get_base_commit_id(worktree); + if (got_object_id_cmp(base_commit_id, head_commit_id) != 0) { + error = got_error(GOT_ERR_REBASE_OUT_OF_DATE); + goto done; + } + + error = got_commit_graph_find_youngest_common_ancestor(&yca_id, + base_commit_id, branch_head_commit_id, 1, repo, + check_cancelled, NULL); + if (error) { + if (error->code == GOT_ERR_ANCESTRY) { + error = got_error_msg(GOT_ERR_ANCESTRY, + "specified branch shares no common " + "ancestry with work tree's branch"); + } + goto done; + } + + if (got_object_id_cmp(base_commit_id, yca_id) == 0) { + struct got_pathlist_head paths; + printf("%s is already based on %s\n", + got_ref_get_name(branch), + got_worktree_get_head_ref_name(worktree)); + error = switch_head_ref(branch, branch_head_commit_id, + worktree, repo); + if (error) + goto done; + error = got_worktree_set_base_commit_id(worktree, repo, + branch_head_commit_id); + if (error) + goto done; + TAILQ_INIT(&paths); + error = got_pathlist_append(&paths, "", NULL); + if (error) + goto done; + error = got_worktree_checkout_files(worktree, + &paths, repo, update_progress, &upa, + check_cancelled, NULL); + got_pathlist_free(&paths, GOT_PATHLIST_FREE_NONE); + if (error) + goto done; + if (upa.did_something) { + char *id_str; + error = got_object_id_str(&id_str, + branch_head_commit_id); + if (error) + goto done; + printf("Updated to %s: %s\n", + got_worktree_get_head_ref_name(worktree), + id_str); + free(id_str); + } else + printf("Already up-to-date\n"); + print_update_progress_stats(&upa); + goto done; + } + } + + commit_id = branch_head_commit_id; + error = got_object_open_as_commit(&commit, repo, commit_id); + if (error) + goto done; + + parent_ids = got_object_commit_get_parent_ids(commit); + pid = STAILQ_FIRST(parent_ids); + if (pid) { + error = collect_commits(&commits, commit_id, &pid->id, + yca_id, got_worktree_get_path_prefix(worktree), + GOT_ERR_REBASE_PATH, repo); + if (error) + goto done; + } + + got_object_commit_close(commit); + commit = NULL; + + if (!continue_rebase) { + error = got_worktree_rebase_prepare(&new_base_branch, + &tmp_branch, &fileindex, worktree, branch, repo); + if (error) + goto done; + } + + if (STAILQ_EMPTY(&commits)) { + if (continue_rebase) { + error = rebase_complete(worktree, fileindex, + branch, tmp_branch, repo, create_backup); + goto done; + } else { + /* Fast-forward the reference of the branch. */ + struct got_object_id *new_head_commit_id; + char *id_str; + error = got_ref_resolve(&new_head_commit_id, repo, + new_base_branch); + if (error) + goto done; + error = got_object_id_str(&id_str, new_head_commit_id); + if (error) + goto done; + printf("Forwarding %s to commit %s\n", + got_ref_get_name(branch), id_str); + free(id_str); + error = got_ref_change_ref(branch, + new_head_commit_id); + if (error) + goto done; + /* No backup needed since objects did not change. */ + create_backup = 0; + } + } + + pid = NULL; + STAILQ_FOREACH(qid, &commits, entry) { + + commit_id = &qid->id; + parent_id = pid ? &pid->id : yca_id; + pid = qid; + + memset(&upa, 0, sizeof(upa)); + error = got_worktree_rebase_merge_files(&merged_paths, + worktree, fileindex, parent_id, commit_id, repo, + update_progress, &upa, check_cancelled, NULL); + if (error) + goto done; + + print_merge_progress_stats(&upa); + if (upa.conflicts > 0 || upa.missing > 0 || + upa.not_deleted > 0 || upa.unversioned > 0) { + if (upa.conflicts > 0) { + error = show_rebase_merge_conflict(&qid->id, + repo); + if (error) + goto done; + } + got_pathlist_free(&merged_paths, GOT_PATHLIST_FREE_PATH); + break; + } + + error = rebase_commit(&merged_paths, worktree, fileindex, + tmp_branch, committer, commit_id, 0, repo); + got_pathlist_free(&merged_paths, GOT_PATHLIST_FREE_PATH); + if (error) + goto done; + } + + if (upa.conflicts > 0 || upa.missing > 0 || + upa.not_deleted > 0 || upa.unversioned > 0) { + error = got_worktree_rebase_postpone(worktree, fileindex); + if (error) + goto done; + if (upa.conflicts > 0 && upa.missing == 0 && + upa.not_deleted == 0 && upa.unversioned == 0) { + error = got_error_msg(GOT_ERR_CONFLICTS, + "conflicts must be resolved before rebasing " + "can continue"); + } else if (upa.conflicts > 0) { + error = got_error_msg(GOT_ERR_CONFLICTS, + "conflicts must be resolved before rebasing " + "can continue; changes destined for some " + "files were not yet merged and should be " + "merged manually if required before the " + "rebase operation is continued"); + } else { + error = got_error_msg(GOT_ERR_CONFLICTS, + "changes destined for some files were not " + "yet merged and should be merged manually " + "if required before the rebase operation " + "is continued"); + } + } else + error = rebase_complete(worktree, fileindex, branch, + tmp_branch, repo, create_backup); +done: + free(cwd); + free(committer); + free(gitconfig_path); + got_object_id_queue_free(&commits); + free(branch_head_commit_id); + free(resume_commit_id); + free(head_commit_id); + free(yca_id); + if (commit) + got_object_commit_close(commit); + if (branch) + got_ref_close(branch); + if (new_base_branch) + got_ref_close(new_base_branch); + if (tmp_branch) + got_ref_close(tmp_branch); + if (head_ref) + got_ref_close(head_ref); + if (worktree) + got_worktree_close(worktree); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + return error; +} + +__dead static void +usage_histedit(void) +{ + fprintf(stderr, "usage: %s histedit [-aCcdeflmX] [-F histedit-script] " + "[branch]\n", getprogname()); + exit(1); +} + +#define GOT_HISTEDIT_PICK 'p' +#define GOT_HISTEDIT_EDIT 'e' +#define GOT_HISTEDIT_FOLD 'f' +#define GOT_HISTEDIT_DROP 'd' +#define GOT_HISTEDIT_MESG 'm' + +static const struct got_histedit_cmd { + unsigned char code; + const char *name; + const char *desc; +} got_histedit_cmds[] = { + { GOT_HISTEDIT_PICK, "pick", "use commit" }, + { GOT_HISTEDIT_EDIT, "edit", "use commit but stop for amending" }, + { GOT_HISTEDIT_FOLD, "fold", "combine with next commit that will " + "be used" }, + { GOT_HISTEDIT_DROP, "drop", "remove commit from history" }, + { GOT_HISTEDIT_MESG, "mesg", + "single-line log message for commit above (open editor if empty)" }, +}; + +struct got_histedit_list_entry { + TAILQ_ENTRY(got_histedit_list_entry) entry; + struct got_object_id *commit_id; + const struct got_histedit_cmd *cmd; + char *logmsg; +}; +TAILQ_HEAD(got_histedit_list, got_histedit_list_entry); + +static const struct got_error * +histedit_write_commit(struct got_object_id *commit_id, const char *cmdname, + FILE *f, struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *logmsg = NULL, *id_str = NULL; + struct got_commit_object *commit = NULL; + int n; + + err = got_object_open_as_commit(&commit, repo, commit_id); + if (err) + goto done; + + err = get_short_logmsg(&logmsg, 34, commit); + if (err) + goto done; + + err = got_object_id_str(&id_str, commit_id); + if (err) + goto done; + + n = fprintf(f, "%s %s %s\n", cmdname, id_str, logmsg); + if (n < 0) + err = got_ferror(f, GOT_ERR_IO); +done: + if (commit) + got_object_commit_close(commit); + free(id_str); + free(logmsg); + return err; +} + +static const struct got_error * +histedit_write_commit_list(struct got_object_id_queue *commits, + FILE *f, int edit_logmsg_only, int fold_only, int drop_only, + int edit_only, struct got_repository *repo) +{ + const struct got_error *err = NULL; + struct got_object_qid *qid; + const char *histedit_cmd = NULL; + + if (STAILQ_EMPTY(commits)) + return got_error(GOT_ERR_EMPTY_HISTEDIT); + + STAILQ_FOREACH(qid, commits, entry) { + histedit_cmd = got_histedit_cmds[0].name; + if (drop_only) + histedit_cmd = "drop"; + else if (edit_only) + histedit_cmd = "edit"; + else if (fold_only && STAILQ_NEXT(qid, entry) != NULL) + histedit_cmd = "fold"; + err = histedit_write_commit(&qid->id, histedit_cmd, f, repo); + if (err) + break; + if (edit_logmsg_only) { + int n = fprintf(f, "%c\n", GOT_HISTEDIT_MESG); + if (n < 0) { + err = got_ferror(f, GOT_ERR_IO); + break; + } + } + } + + return err; +} + +static const struct got_error * +write_cmd_list(FILE *f, const char *branch_name, + struct got_object_id_queue *commits) +{ + const struct got_error *err = NULL; + size_t i; + int n; + char *id_str; + struct got_object_qid *qid; + + qid = STAILQ_FIRST(commits); + err = got_object_id_str(&id_str, &qid->id); + if (err) + return err; + + n = fprintf(f, + "# Editing the history of branch '%s' starting at\n" + "# commit %s\n" + "# Commits will be processed in order from top to " + "bottom of this file.\n", branch_name, id_str); + if (n < 0) { + err = got_ferror(f, GOT_ERR_IO); + goto done; + } + + n = fprintf(f, "# Available histedit commands:\n"); + if (n < 0) { + err = got_ferror(f, GOT_ERR_IO); + goto done; + } + + for (i = 0; i < nitems(got_histedit_cmds); i++) { + const struct got_histedit_cmd *cmd = &got_histedit_cmds[i]; + n = fprintf(f, "# %s (%c): %s\n", cmd->name, cmd->code, + cmd->desc); + if (n < 0) { + err = got_ferror(f, GOT_ERR_IO); + break; + } + } +done: + free(id_str); + return err; +} + +static const struct got_error * +histedit_syntax_error(int lineno) +{ + static char msg[42]; + int ret; + + ret = snprintf(msg, sizeof(msg), "histedit syntax error on line %d", + lineno); + if (ret < 0 || (size_t)ret >= sizeof(msg)) + return got_error(GOT_ERR_HISTEDIT_SYNTAX); + + return got_error_msg(GOT_ERR_HISTEDIT_SYNTAX, msg); +} + +static const struct got_error * +append_folded_commit_msg(char **new_msg, struct got_histedit_list_entry *hle, + char *logmsg, struct got_repository *repo) +{ + const struct got_error *err; + struct got_commit_object *folded_commit = NULL; + char *id_str, *folded_logmsg = NULL; + + err = got_object_id_str(&id_str, hle->commit_id); + if (err) + return err; + + err = got_object_open_as_commit(&folded_commit, repo, hle->commit_id); + if (err) + goto done; + + err = got_object_commit_get_logmsg(&folded_logmsg, folded_commit); + if (err) + goto done; + if (asprintf(new_msg, "%s%s# log message of folded commit %s: %s", + logmsg ? logmsg : "", logmsg ? "\n" : "", id_str, + folded_logmsg) == -1) { + err = got_error_from_errno("asprintf"); + } +done: + if (folded_commit) + got_object_commit_close(folded_commit); + free(id_str); + free(folded_logmsg); + return err; +} + +static struct got_histedit_list_entry * +get_folded_commits(struct got_histedit_list_entry *hle) +{ + struct got_histedit_list_entry *prev, *folded = NULL; + + prev = TAILQ_PREV(hle, got_histedit_list, entry); + while (prev && (prev->cmd->code == GOT_HISTEDIT_FOLD || + prev->cmd->code == GOT_HISTEDIT_DROP)) { + if (prev->cmd->code == GOT_HISTEDIT_FOLD) + folded = prev; + prev = TAILQ_PREV(prev, got_histedit_list, entry); + } + + return folded; +} + +static const struct got_error * +histedit_edit_logmsg(struct got_histedit_list_entry *hle, + struct got_repository *repo) +{ + char *logmsg_path = NULL, *id_str = NULL, *orig_logmsg = NULL; + char *logmsg = NULL, *new_msg = NULL, *editor = NULL; + const struct got_error *err = NULL; + struct got_commit_object *commit = NULL; + int logmsg_len; + int fd = -1; + struct got_histedit_list_entry *folded = NULL; + + err = got_object_open_as_commit(&commit, repo, hle->commit_id); + if (err) + return err; + + folded = get_folded_commits(hle); + if (folded) { + while (folded != hle) { + if (folded->cmd->code == GOT_HISTEDIT_DROP) { + folded = TAILQ_NEXT(folded, entry); + continue; + } + err = append_folded_commit_msg(&new_msg, folded, + logmsg, repo); + if (err) + goto done; + free(logmsg); + logmsg = new_msg; + folded = TAILQ_NEXT(folded, entry); + } + } + + err = got_object_id_str(&id_str, hle->commit_id); + if (err) + goto done; + err = got_object_commit_get_logmsg(&orig_logmsg, commit); + if (err) + goto done; + logmsg_len = asprintf(&new_msg, + "%s\n# original log message of commit %s: %s", + logmsg ? logmsg : "", id_str, orig_logmsg); + if (logmsg_len == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + free(logmsg); + logmsg = new_msg; + + err = got_object_id_str(&id_str, hle->commit_id); + if (err) + goto done; + + err = got_opentemp_named_fd(&logmsg_path, &fd, + GOT_TMPDIR_STR "/got-logmsg", ""); + if (err) + goto done; + + if (write(fd, logmsg, logmsg_len) == -1) { + err = got_error_from_errno2("write", logmsg_path); + goto done; + } + if (close(fd) == -1) { + err = got_error_from_errno2("close", logmsg_path); + goto done; + } + fd = -1; + + err = get_editor(&editor); + if (err) + goto done; + + err = edit_logmsg(&hle->logmsg, editor, logmsg_path, logmsg, + logmsg_len, 0); + if (err) { + if (err->code != GOT_ERR_COMMIT_MSG_EMPTY) + goto done; + err = NULL; + hle->logmsg = strdup(new_msg); + if (hle->logmsg == NULL) + err = got_error_from_errno("strdup"); + } +done: + if (fd != -1 && close(fd) == -1 && err == NULL) + err = got_error_from_errno2("close", logmsg_path); + if (logmsg_path && unlink(logmsg_path) != 0 && err == NULL) + err = got_error_from_errno2("unlink", logmsg_path); + free(logmsg_path); + free(logmsg); + free(orig_logmsg); + free(editor); + if (commit) + got_object_commit_close(commit); + return err; +} + +static const struct got_error * +histedit_parse_list(struct got_histedit_list *histedit_cmds, + FILE *f, struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *line = NULL, *p, *end; + size_t i, linesize = 0; + ssize_t linelen; + int lineno = 0, lastcmd = -1; + const struct got_histedit_cmd *cmd; + struct got_object_id *commit_id = NULL; + struct got_histedit_list_entry *hle = NULL; + + for (;;) { + linelen = getline(&line, &linesize, f); + if (linelen == -1) { + const struct got_error *getline_err; + if (feof(f)) + break; + getline_err = got_error_from_errno("getline"); + err = got_ferror(f, getline_err->code); + break; + } + lineno++; + p = line; + while (isspace((unsigned char)p[0])) + p++; + if (p[0] == '#' || p[0] == '\0') + continue; + cmd = NULL; + for (i = 0; i < nitems(got_histedit_cmds); i++) { + cmd = &got_histedit_cmds[i]; + if (strncmp(cmd->name, p, strlen(cmd->name)) == 0 && + isspace((unsigned char)p[strlen(cmd->name)])) { + p += strlen(cmd->name); + break; + } + if (p[0] == cmd->code && isspace((unsigned char)p[1])) { + p++; + break; + } + } + if (i == nitems(got_histedit_cmds)) { + err = histedit_syntax_error(lineno); + break; + } + while (isspace((unsigned char)p[0])) + p++; + if (cmd->code == GOT_HISTEDIT_MESG) { + if (lastcmd != GOT_HISTEDIT_PICK && + lastcmd != GOT_HISTEDIT_EDIT) { + err = got_error(GOT_ERR_HISTEDIT_CMD); + break; + } + if (p[0] == '\0') { + err = histedit_edit_logmsg(hle, repo); + if (err) + break; + } else { + hle->logmsg = strdup(p); + if (hle->logmsg == NULL) { + err = got_error_from_errno("strdup"); + break; + } + } + lastcmd = cmd->code; + continue; + } else { + end = p; + while (end[0] && !isspace((unsigned char)end[0])) + end++; + *end = '\0'; + + err = got_object_resolve_id_str(&commit_id, repo, p); + if (err) { + /* override error code */ + err = histedit_syntax_error(lineno); + break; + } + } + hle = malloc(sizeof(*hle)); + if (hle == NULL) { + err = got_error_from_errno("malloc"); + break; + } + hle->cmd = cmd; + hle->commit_id = commit_id; + hle->logmsg = NULL; + commit_id = NULL; + TAILQ_INSERT_TAIL(histedit_cmds, hle, entry); + lastcmd = cmd->code; + } + + free(line); + free(commit_id); + return err; +} + +static const struct got_error * +histedit_check_script(struct got_histedit_list *histedit_cmds, + struct got_object_id_queue *commits, struct got_repository *repo) +{ + const struct got_error *err = NULL; + struct got_object_qid *qid; + struct got_histedit_list_entry *hle; + static char msg[92]; + char *id_str; + + if (TAILQ_EMPTY(histedit_cmds)) + return got_error_msg(GOT_ERR_EMPTY_HISTEDIT, + "histedit script contains no commands"); + if (STAILQ_EMPTY(commits)) + return got_error(GOT_ERR_EMPTY_HISTEDIT); + + TAILQ_FOREACH(hle, histedit_cmds, entry) { + struct got_histedit_list_entry *hle2; + TAILQ_FOREACH(hle2, histedit_cmds, entry) { + if (hle == hle2) + continue; + if (got_object_id_cmp(hle->commit_id, + hle2->commit_id) != 0) + continue; + err = got_object_id_str(&id_str, hle->commit_id); + if (err) + return err; + snprintf(msg, sizeof(msg), "commit %s is listed " + "more than once in histedit script", id_str); + free(id_str); + return got_error_msg(GOT_ERR_HISTEDIT_CMD, msg); + } + } + + STAILQ_FOREACH(qid, commits, entry) { + TAILQ_FOREACH(hle, histedit_cmds, entry) { + if (got_object_id_cmp(&qid->id, hle->commit_id) == 0) + break; + } + if (hle == NULL) { + err = got_object_id_str(&id_str, &qid->id); + if (err) + return err; + snprintf(msg, sizeof(msg), + "commit %s missing from histedit script", id_str); + free(id_str); + return got_error_msg(GOT_ERR_HISTEDIT_CMD, msg); + } + } + + hle = TAILQ_LAST(histedit_cmds, got_histedit_list); + if (hle && hle->cmd->code == GOT_HISTEDIT_FOLD) + return got_error_msg(GOT_ERR_HISTEDIT_CMD, + "last commit in histedit script cannot be folded"); + + return NULL; +} + +static const struct got_error * +histedit_run_editor(struct got_histedit_list *histedit_cmds, + const char *path, struct got_object_id_queue *commits, + struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *editor; + FILE *f = NULL; + + err = get_editor(&editor); + if (err) + return err; + + if (spawn_editor(editor, path) == -1) { + err = got_error_from_errno("failed spawning editor"); + goto done; + } + + f = fopen(path, "re"); + if (f == NULL) { + err = got_error_from_errno("fopen"); + goto done; + } + err = histedit_parse_list(histedit_cmds, f, repo); + if (err) + goto done; + + err = histedit_check_script(histedit_cmds, commits, repo); +done: + if (f && fclose(f) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + free(editor); + return err; +} + +static const struct got_error * +histedit_edit_list_retry(struct got_histedit_list *, const struct got_error *, + struct got_object_id_queue *, const char *, const char *, + struct got_repository *); + +static const struct got_error * +histedit_edit_script(struct got_histedit_list *histedit_cmds, + struct got_object_id_queue *commits, const char *branch_name, + int edit_logmsg_only, int fold_only, int drop_only, int edit_only, + struct got_repository *repo) +{ + const struct got_error *err; + FILE *f = NULL; + char *path = NULL; + + err = got_opentemp_named(&path, &f, "got-histedit", ""); + if (err) + return err; + + err = write_cmd_list(f, branch_name, commits); + if (err) + goto done; + + err = histedit_write_commit_list(commits, f, edit_logmsg_only, + fold_only, drop_only, edit_only, repo); + if (err) + goto done; + + if (drop_only || edit_logmsg_only || fold_only || edit_only) { + rewind(f); + err = histedit_parse_list(histedit_cmds, f, repo); + } else { + if (fclose(f) == EOF) { + err = got_error_from_errno("fclose"); + goto done; + } + f = NULL; + err = histedit_run_editor(histedit_cmds, path, commits, repo); + if (err) { + if (err->code != GOT_ERR_HISTEDIT_SYNTAX && + err->code != GOT_ERR_HISTEDIT_CMD) + goto done; + err = histedit_edit_list_retry(histedit_cmds, err, + commits, path, branch_name, repo); + } + } +done: + if (f && fclose(f) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + if (path && unlink(path) != 0 && err == NULL) + err = got_error_from_errno2("unlink", path); + free(path); + return err; +} + +static const struct got_error * +histedit_save_list(struct got_histedit_list *histedit_cmds, + struct got_worktree *worktree, struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *path = NULL; + FILE *f = NULL; + struct got_histedit_list_entry *hle; + struct got_commit_object *commit = NULL; + + err = got_worktree_get_histedit_script_path(&path, worktree); + if (err) + return err; + + f = fopen(path, "we"); + if (f == NULL) { + err = got_error_from_errno2("fopen", path); + goto done; + } + TAILQ_FOREACH(hle, histedit_cmds, entry) { + err = histedit_write_commit(hle->commit_id, hle->cmd->name, f, + repo); + if (err) + break; + + if (hle->logmsg) { + int n = fprintf(f, "%c %s\n", + GOT_HISTEDIT_MESG, hle->logmsg); + if (n < 0) { + err = got_ferror(f, GOT_ERR_IO); + break; + } + } + } +done: + if (f && fclose(f) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + free(path); + if (commit) + got_object_commit_close(commit); + return err; +} + +static void +histedit_free_list(struct got_histedit_list *histedit_cmds) +{ + struct got_histedit_list_entry *hle; + + while ((hle = TAILQ_FIRST(histedit_cmds))) { + TAILQ_REMOVE(histedit_cmds, hle, entry); + free(hle); + } +} + +static const struct got_error * +histedit_load_list(struct got_histedit_list *histedit_cmds, + const char *path, struct got_repository *repo) +{ + const struct got_error *err = NULL; + FILE *f = NULL; + + f = fopen(path, "re"); + if (f == NULL) { + err = got_error_from_errno2("fopen", path); + goto done; + } + + err = histedit_parse_list(histedit_cmds, f, repo); +done: + if (f && fclose(f) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + return err; +} + +static const struct got_error * +histedit_edit_list_retry(struct got_histedit_list *histedit_cmds, + const struct got_error *edit_err, struct got_object_id_queue *commits, + const char *path, const char *branch_name, struct got_repository *repo) +{ + const struct got_error *err = NULL, *prev_err = edit_err; + int resp = ' '; + + while (resp != 'c' && resp != 'r' && resp != 'a') { + printf("%s: %s\n(c)ontinue editing, (r)estart editing, " + "or (a)bort: ", getprogname(), prev_err->msg); + resp = getchar(); + if (resp == '\n') + resp = getchar(); + if (resp == 'c') { + histedit_free_list(histedit_cmds); + err = histedit_run_editor(histedit_cmds, path, commits, + repo); + if (err) { + if (err->code != GOT_ERR_HISTEDIT_SYNTAX && + err->code != GOT_ERR_HISTEDIT_CMD) + break; + prev_err = err; + resp = ' '; + continue; + } + break; + } else if (resp == 'r') { + histedit_free_list(histedit_cmds); + err = histedit_edit_script(histedit_cmds, + commits, branch_name, 0, 0, 0, 0, repo); + if (err) { + if (err->code != GOT_ERR_HISTEDIT_SYNTAX && + err->code != GOT_ERR_HISTEDIT_CMD) + break; + prev_err = err; + resp = ' '; + continue; + } + break; + } else if (resp == 'a') { + err = got_error(GOT_ERR_HISTEDIT_CANCEL); + break; + } else + printf("invalid response '%c'\n", resp); + } + + return err; +} + +static const struct got_error * +histedit_complete(struct got_worktree *worktree, + struct got_fileindex *fileindex, struct got_reference *tmp_branch, + struct got_reference *branch, struct got_repository *repo) +{ + printf("Switching work tree to %s\n", + got_ref_get_symref_target(branch)); + return got_worktree_histedit_complete(worktree, fileindex, tmp_branch, + branch, repo); +} + +static const struct got_error * +show_histedit_progress(struct got_commit_object *commit, + struct got_histedit_list_entry *hle, struct got_object_id *new_id) +{ + const struct got_error *err; + char *old_id_str = NULL, *new_id_str = NULL, *logmsg = NULL; + + err = got_object_id_str(&old_id_str, hle->commit_id); + if (err) + goto done; + + if (new_id) { + err = got_object_id_str(&new_id_str, new_id); + if (err) + goto done; + } + + old_id_str[12] = '\0'; + if (new_id_str) + new_id_str[12] = '\0'; + + if (hle->logmsg) { + logmsg = strdup(hle->logmsg); + if (logmsg == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + trim_logmsg(logmsg, 42); + } else { + err = get_short_logmsg(&logmsg, 42, commit); + if (err) + goto done; + } + + switch (hle->cmd->code) { + case GOT_HISTEDIT_PICK: + case GOT_HISTEDIT_EDIT: + printf("%s -> %s: %s\n", old_id_str, + new_id_str ? new_id_str : "no-op change", logmsg); + break; + case GOT_HISTEDIT_DROP: + case GOT_HISTEDIT_FOLD: + printf("%s -> %s commit: %s\n", old_id_str, hle->cmd->name, + logmsg); + break; + default: + break; + } +done: + free(old_id_str); + free(new_id_str); + return err; +} + +static const struct got_error * +histedit_commit(struct got_pathlist_head *merged_paths, + struct got_worktree *worktree, struct got_fileindex *fileindex, + struct got_reference *tmp_branch, struct got_histedit_list_entry *hle, + const char *committer, int allow_conflict, struct got_repository *repo) +{ + const struct got_error *err; + struct got_commit_object *commit; + struct got_object_id *new_commit_id; + + if ((hle->cmd->code == GOT_HISTEDIT_EDIT || get_folded_commits(hle)) + && hle->logmsg == NULL) { + err = histedit_edit_logmsg(hle, repo); + if (err) + return err; + } + + err = got_object_open_as_commit(&commit, repo, hle->commit_id); + if (err) + return err; + + err = got_worktree_histedit_commit(&new_commit_id, merged_paths, + worktree, fileindex, tmp_branch, committer, commit, hle->commit_id, + hle->logmsg, allow_conflict, repo); + if (err) { + if (err->code != GOT_ERR_COMMIT_NO_CHANGES) + goto done; + err = show_histedit_progress(commit, hle, NULL); + } else { + err = show_histedit_progress(commit, hle, new_commit_id); + free(new_commit_id); + } +done: + got_object_commit_close(commit); + return err; +} + +static const struct got_error * +histedit_skip_commit(struct got_histedit_list_entry *hle, + struct got_worktree *worktree, struct got_repository *repo) +{ + const struct got_error *error; + struct got_commit_object *commit; + + error = got_worktree_histedit_skip_commit(worktree, hle->commit_id, + repo); + if (error) + return error; + + error = got_object_open_as_commit(&commit, repo, hle->commit_id); + if (error) + return error; + + error = show_histedit_progress(commit, hle, NULL); + got_object_commit_close(commit); + return error; +} + +static const struct got_error * +check_local_changes(void *arg, unsigned char status, + unsigned char staged_status, const char *path, + struct got_object_id *blob_id, struct got_object_id *staged_blob_id, + struct got_object_id *commit_id, int dirfd, const char *de_name) +{ + int *have_local_changes = arg; + + switch (status) { + case GOT_STATUS_ADD: + case GOT_STATUS_DELETE: + case GOT_STATUS_MODIFY: + case GOT_STATUS_CONFLICT: + *have_local_changes = 1; + return got_error(GOT_ERR_CANCELLED); + default: + break; + } + + switch (staged_status) { + case GOT_STATUS_ADD: + case GOT_STATUS_DELETE: + case GOT_STATUS_MODIFY: + *have_local_changes = 1; + return got_error(GOT_ERR_CANCELLED); + default: + break; + } + + return NULL; +} + +static const struct got_error * +cmd_histedit(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_worktree *worktree = NULL; + struct got_fileindex *fileindex = NULL; + struct got_repository *repo = NULL; + char *cwd = NULL, *committer = NULL, *gitconfig_path = NULL; + struct got_reference *branch = NULL; + struct got_reference *tmp_branch = NULL; + struct got_object_id *resume_commit_id = NULL; + struct got_object_id *base_commit_id = NULL; + struct got_object_id *head_commit_id = NULL; + struct got_commit_object *commit = NULL; + int ch, rebase_in_progress = 0, merge_in_progress = 0; + struct got_update_progress_arg upa; + int edit_in_progress = 0, abort_edit = 0, continue_edit = 0; + int drop_only = 0, edit_logmsg_only = 0, fold_only = 0, edit_only = 0; + int allow_conflict = 0, list_backups = 0, delete_backups = 0; + const char *edit_script_path = NULL; + struct got_object_id_queue commits; + struct got_pathlist_head merged_paths; + const struct got_object_id_queue *parent_ids; + struct got_object_qid *pid; + struct got_histedit_list histedit_cmds; + struct got_histedit_list_entry *hle; + int *pack_fds = NULL; + + STAILQ_INIT(&commits); + TAILQ_INIT(&histedit_cmds); + TAILQ_INIT(&merged_paths); + memset(&upa, 0, sizeof(upa)); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "aCcdeF:flmX")) != -1) { + switch (ch) { + case 'a': + abort_edit = 1; + break; + case 'C': + allow_conflict = 1; + break; + case 'c': + continue_edit = 1; + break; + case 'd': + drop_only = 1; + break; + case 'e': + edit_only = 1; + break; + case 'F': + edit_script_path = optarg; + break; + case 'f': + fold_only = 1; + break; + case 'l': + list_backups = 1; + break; + case 'm': + edit_logmsg_only = 1; + break; + case 'X': + delete_backups = 1; + break; + default: + usage_histedit(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (abort_edit && allow_conflict) + option_conflict('a', 'C'); + if (abort_edit && continue_edit) + option_conflict('a', 'c'); + if (edit_script_path && allow_conflict) + option_conflict('F', 'C'); + if (edit_script_path && edit_logmsg_only) + option_conflict('F', 'm'); + if (abort_edit && edit_logmsg_only) + option_conflict('a', 'm'); + if (edit_logmsg_only && allow_conflict) + option_conflict('m', 'C'); + if (continue_edit && edit_logmsg_only) + option_conflict('c', 'm'); + if (abort_edit && fold_only) + option_conflict('a', 'f'); + if (fold_only && allow_conflict) + option_conflict('f', 'C'); + if (continue_edit && fold_only) + option_conflict('c', 'f'); + if (fold_only && edit_logmsg_only) + option_conflict('f', 'm'); + if (edit_script_path && fold_only) + option_conflict('F', 'f'); + if (abort_edit && edit_only) + option_conflict('a', 'e'); + if (continue_edit && edit_only) + option_conflict('c', 'e'); + if (edit_only && edit_logmsg_only) + option_conflict('e', 'm'); + if (edit_script_path && edit_only) + option_conflict('F', 'e'); + if (fold_only && edit_only) + option_conflict('f', 'e'); + if (drop_only && abort_edit) + option_conflict('d', 'a'); + if (drop_only && allow_conflict) + option_conflict('d', 'C'); + if (drop_only && continue_edit) + option_conflict('d', 'c'); + if (drop_only && edit_logmsg_only) + option_conflict('d', 'm'); + if (drop_only && edit_only) + option_conflict('d', 'e'); + if (drop_only && edit_script_path) + option_conflict('d', 'F'); + if (drop_only && fold_only) + option_conflict('d', 'f'); + if (list_backups) { + if (abort_edit) + option_conflict('l', 'a'); + if (allow_conflict) + option_conflict('l', 'C'); + if (continue_edit) + option_conflict('l', 'c'); + if (edit_script_path) + option_conflict('l', 'F'); + if (edit_logmsg_only) + option_conflict('l', 'm'); + if (drop_only) + option_conflict('l', 'd'); + if (fold_only) + option_conflict('l', 'f'); + if (edit_only) + option_conflict('l', 'e'); + if (delete_backups) + option_conflict('l', 'X'); + if (argc != 0 && argc != 1) + usage_histedit(); + } else if (delete_backups) { + if (abort_edit) + option_conflict('X', 'a'); + if (allow_conflict) + option_conflict('X', 'C'); + if (continue_edit) + option_conflict('X', 'c'); + if (drop_only) + option_conflict('X', 'd'); + if (edit_script_path) + option_conflict('X', 'F'); + if (edit_logmsg_only) + option_conflict('X', 'm'); + if (fold_only) + option_conflict('X', 'f'); + if (edit_only) + option_conflict('X', 'e'); + if (list_backups) + option_conflict('X', 'l'); + if (argc != 0 && argc != 1) + usage_histedit(); + } else if (allow_conflict && !continue_edit) + errx(1, "-C option requires -c"); + else if (argc != 0) + usage_histedit(); + + /* + * This command cannot apply unveil(2) in all cases because the + * user may choose to run an editor to edit the histedit script + * and to edit individual commit log messages. + * unveil(2) traverses exec(2); if an editor is used we have to + * apply unveil after edit script and log messages have been written. + * XXX TODO: Make use of unveil(2) where possible. + */ + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_open(&worktree, cwd); + if (error) { + if (list_backups || delete_backups) { + if (error->code != GOT_ERR_NOT_WORKTREE) + goto done; + } else { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, + "histedit", cwd); + goto done; + } + } + + if (list_backups || delete_backups) { + error = got_repo_open(&repo, + worktree ? got_worktree_get_repo_path(worktree) : cwd, + NULL, pack_fds); + if (error != NULL) + goto done; + error = apply_unveil(got_repo_get_path(repo), 0, + worktree ? got_worktree_get_root_path(worktree) : NULL); + if (error) + goto done; + error = process_backup_refs( + GOT_WORKTREE_HISTEDIT_BACKUP_REF_PREFIX, + argc == 1 ? argv[0] : NULL, delete_backups, repo); + goto done; /* nothing else to do */ + } + + error = get_gitconfig_path(&gitconfig_path); + if (error) + goto done; + error = got_repo_open(&repo, got_worktree_get_repo_path(worktree), + gitconfig_path, pack_fds); + if (error != NULL) + goto done; + + if (worktree != NULL && !list_backups && !delete_backups) { + error = worktree_has_logmsg_ref("histedit", worktree, repo); + if (error) + goto done; + } + + error = got_worktree_rebase_in_progress(&rebase_in_progress, worktree); + if (error) + goto done; + if (rebase_in_progress) { + error = got_error(GOT_ERR_REBASING); + goto done; + } + + error = got_worktree_merge_in_progress(&merge_in_progress, worktree, + repo); + if (error) + goto done; + if (merge_in_progress) { + error = got_error(GOT_ERR_MERGE_BUSY); + goto done; + } + + error = got_worktree_histedit_in_progress(&edit_in_progress, worktree); + if (error) + goto done; + + if (edit_in_progress && edit_logmsg_only) { + error = got_error_msg(GOT_ERR_HISTEDIT_BUSY, + "histedit operation is in progress in this " + "work tree and must be continued or aborted " + "before the -m option can be used"); + goto done; + } + if (edit_in_progress && drop_only) { + error = got_error_msg(GOT_ERR_HISTEDIT_BUSY, + "histedit operation is in progress in this " + "work tree and must be continued or aborted " + "before the -d option can be used"); + goto done; + } + if (edit_in_progress && fold_only) { + error = got_error_msg(GOT_ERR_HISTEDIT_BUSY, + "histedit operation is in progress in this " + "work tree and must be continued or aborted " + "before the -f option can be used"); + goto done; + } + if (edit_in_progress && edit_only) { + error = got_error_msg(GOT_ERR_HISTEDIT_BUSY, + "histedit operation is in progress in this " + "work tree and must be continued or aborted " + "before the -e option can be used"); + goto done; + } + + if (edit_in_progress && abort_edit) { + error = got_worktree_histedit_continue(&resume_commit_id, + &tmp_branch, &branch, &base_commit_id, &fileindex, + worktree, repo); + if (error) + goto done; + printf("Switching work tree to %s\n", + got_ref_get_symref_target(branch)); + error = got_worktree_histedit_abort(worktree, fileindex, repo, + branch, base_commit_id, abort_progress, &upa); + if (error) + goto done; + printf("Histedit of %s aborted\n", + got_ref_get_symref_target(branch)); + print_merge_progress_stats(&upa); + goto done; /* nothing else to do */ + } else if (abort_edit) { + error = got_error(GOT_ERR_NOT_HISTEDIT); + goto done; + } + + error = get_author(&committer, repo, worktree); + if (error) + goto done; + + if (continue_edit) { + char *path; + + if (!edit_in_progress) { + error = got_error(GOT_ERR_NOT_HISTEDIT); + goto done; + } + + error = got_worktree_get_histedit_script_path(&path, worktree); + if (error) + goto done; + + error = histedit_load_list(&histedit_cmds, path, repo); + free(path); + if (error) + goto done; + + error = got_worktree_histedit_continue(&resume_commit_id, + &tmp_branch, &branch, &base_commit_id, &fileindex, + worktree, repo); + if (error) + goto done; + + error = got_ref_resolve(&head_commit_id, repo, branch); + if (error) + goto done; + + error = got_object_open_as_commit(&commit, repo, + head_commit_id); + if (error) + goto done; + parent_ids = got_object_commit_get_parent_ids(commit); + pid = STAILQ_FIRST(parent_ids); + if (pid == NULL) { + error = got_error(GOT_ERR_EMPTY_HISTEDIT); + goto done; + } + error = collect_commits(&commits, head_commit_id, &pid->id, + base_commit_id, got_worktree_get_path_prefix(worktree), + GOT_ERR_HISTEDIT_PATH, repo); + got_object_commit_close(commit); + commit = NULL; + if (error) + goto done; + } else { + if (edit_in_progress) { + error = got_error(GOT_ERR_HISTEDIT_BUSY); + goto done; + } + + error = got_ref_open(&branch, repo, + got_worktree_get_head_ref_name(worktree), 0); + if (error != NULL) + goto done; + + if (strncmp(got_ref_get_name(branch), "refs/heads/", 11) != 0) { + error = got_error_msg(GOT_ERR_COMMIT_BRANCH, + "will not edit commit history of a branch outside " + "the \"refs/heads/\" reference namespace"); + goto done; + } + + error = got_ref_resolve(&head_commit_id, repo, branch); + got_ref_close(branch); + branch = NULL; + if (error) + goto done; + + error = got_object_open_as_commit(&commit, repo, + head_commit_id); + if (error) + goto done; + parent_ids = got_object_commit_get_parent_ids(commit); + pid = STAILQ_FIRST(parent_ids); + if (pid == NULL) { + error = got_error(GOT_ERR_EMPTY_HISTEDIT); + goto done; + } + error = collect_commits(&commits, head_commit_id, &pid->id, + got_worktree_get_base_commit_id(worktree), + got_worktree_get_path_prefix(worktree), + GOT_ERR_HISTEDIT_PATH, repo); + got_object_commit_close(commit); + commit = NULL; + if (error) + goto done; + + if (STAILQ_EMPTY(&commits)) { + error = got_error(GOT_ERR_EMPTY_HISTEDIT); + goto done; + } + + error = got_worktree_histedit_prepare(&tmp_branch, &branch, + &base_commit_id, &fileindex, worktree, repo); + if (error) + goto done; + + if (edit_script_path) { + error = histedit_load_list(&histedit_cmds, + edit_script_path, repo); + if (error) { + got_worktree_histedit_abort(worktree, fileindex, + repo, branch, base_commit_id, + abort_progress, &upa); + print_merge_progress_stats(&upa); + goto done; + } + } else { + const char *branch_name; + branch_name = got_ref_get_symref_target(branch); + if (strncmp(branch_name, "refs/heads/", 11) == 0) + branch_name += 11; + error = histedit_edit_script(&histedit_cmds, &commits, + branch_name, edit_logmsg_only, fold_only, + drop_only, edit_only, repo); + if (error) { + got_worktree_histedit_abort(worktree, fileindex, + repo, branch, base_commit_id, + abort_progress, &upa); + print_merge_progress_stats(&upa); + goto done; + } + + } + + error = histedit_save_list(&histedit_cmds, worktree, + repo); + if (error) { + got_worktree_histedit_abort(worktree, fileindex, + repo, branch, base_commit_id, + abort_progress, &upa); + print_merge_progress_stats(&upa); + goto done; + } + + } + + error = histedit_check_script(&histedit_cmds, &commits, repo); + if (error) + goto done; + + TAILQ_FOREACH(hle, &histedit_cmds, entry) { + if (resume_commit_id) { + if (got_object_id_cmp(hle->commit_id, + resume_commit_id) != 0) + continue; + + resume_commit_id = NULL; + if (hle->cmd->code == GOT_HISTEDIT_DROP || + hle->cmd->code == GOT_HISTEDIT_FOLD) { + error = histedit_skip_commit(hle, worktree, + repo); + if (error) + goto done; + } else { + struct got_pathlist_head paths; + int have_changes = 0; + + TAILQ_INIT(&paths); + error = got_pathlist_append(&paths, "", NULL); + if (error) + goto done; + error = got_worktree_status(worktree, &paths, + repo, 0, check_local_changes, &have_changes, + check_cancelled, NULL); + got_pathlist_free(&paths, + GOT_PATHLIST_FREE_NONE); + if (error) { + if (error->code != GOT_ERR_CANCELLED) + goto done; + if (sigint_received || sigpipe_received) + goto done; + } + if (have_changes) { + error = histedit_commit(NULL, worktree, + fileindex, tmp_branch, hle, + committer, allow_conflict, repo); + if (error) + goto done; + } else { + error = got_object_open_as_commit( + &commit, repo, hle->commit_id); + if (error) + goto done; + error = show_histedit_progress(commit, + hle, NULL); + got_object_commit_close(commit); + commit = NULL; + if (error) + goto done; + } + } + continue; + } + + if (hle->cmd->code == GOT_HISTEDIT_DROP) { + error = histedit_skip_commit(hle, worktree, repo); + if (error) + goto done; + continue; + } + + error = got_object_open_as_commit(&commit, repo, + hle->commit_id); + if (error) + goto done; + parent_ids = got_object_commit_get_parent_ids(commit); + pid = STAILQ_FIRST(parent_ids); + + error = got_worktree_histedit_merge_files(&merged_paths, + worktree, fileindex, &pid->id, hle->commit_id, repo, + update_progress, &upa, check_cancelled, NULL); + if (error) + goto done; + got_object_commit_close(commit); + commit = NULL; + + print_merge_progress_stats(&upa); + if (upa.conflicts > 0 || upa.missing > 0 || + upa.not_deleted > 0 || upa.unversioned > 0) { + if (upa.conflicts > 0) { + error = show_rebase_merge_conflict( + hle->commit_id, repo); + if (error) + goto done; + } + got_pathlist_free(&merged_paths, GOT_PATHLIST_FREE_PATH); + break; + } + + if (hle->cmd->code == GOT_HISTEDIT_EDIT) { + char *id_str; + error = got_object_id_str(&id_str, hle->commit_id); + if (error) + goto done; + printf("Stopping histedit for amending commit %s\n", + id_str); + free(id_str); + got_pathlist_free(&merged_paths, GOT_PATHLIST_FREE_PATH); + error = got_worktree_histedit_postpone(worktree, + fileindex); + goto done; + } + + if (hle->cmd->code == GOT_HISTEDIT_FOLD) { + error = histedit_skip_commit(hle, worktree, repo); + if (error) + goto done; + continue; + } + + error = histedit_commit(&merged_paths, worktree, fileindex, + tmp_branch, hle, committer, allow_conflict, repo); + got_pathlist_free(&merged_paths, GOT_PATHLIST_FREE_PATH); + if (error) + goto done; + } + + if (upa.conflicts > 0 || upa.missing > 0 || + upa.not_deleted > 0 || upa.unversioned > 0) { + error = got_worktree_histedit_postpone(worktree, fileindex); + if (error) + goto done; + if (upa.conflicts > 0 && upa.missing == 0 && + upa.not_deleted == 0 && upa.unversioned == 0) { + error = got_error_msg(GOT_ERR_CONFLICTS, + "conflicts must be resolved before histedit " + "can continue"); + } else if (upa.conflicts > 0) { + error = got_error_msg(GOT_ERR_CONFLICTS, + "conflicts must be resolved before histedit " + "can continue; changes destined for some " + "files were not yet merged and should be " + "merged manually if required before the " + "histedit operation is continued"); + } else { + error = got_error_msg(GOT_ERR_CONFLICTS, + "changes destined for some files were not " + "yet merged and should be merged manually " + "if required before the histedit operation " + "is continued"); + } + } else + error = histedit_complete(worktree, fileindex, tmp_branch, + branch, repo); +done: + free(cwd); + free(committer); + free(gitconfig_path); + got_object_id_queue_free(&commits); + histedit_free_list(&histedit_cmds); + free(head_commit_id); + free(base_commit_id); + free(resume_commit_id); + if (commit) + got_object_commit_close(commit); + if (branch) + got_ref_close(branch); + if (tmp_branch) + got_ref_close(tmp_branch); + if (worktree) + got_worktree_close(worktree); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + return error; +} + +__dead static void +usage_integrate(void) +{ + fprintf(stderr, "usage: %s integrate branch\n", getprogname()); + exit(1); +} + +static const struct got_error * +cmd_integrate(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + char *cwd = NULL, *refname = NULL, *base_refname = NULL; + const char *branch_arg = NULL; + struct got_reference *branch_ref = NULL, *base_branch_ref = NULL; + struct got_fileindex *fileindex = NULL; + struct got_object_id *commit_id = NULL, *base_commit_id = NULL; + int ch; + struct got_update_progress_arg upa; + int *pack_fds = NULL; + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "")) != -1) { + switch (ch) { + default: + usage_integrate(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (argc != 1) + usage_integrate(); + branch_arg = argv[0]; + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_open(&worktree, cwd); + if (error) { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, "integrate", + cwd); + goto done; + } + + error = check_rebase_or_histedit_in_progress(worktree); + if (error) + goto done; + + error = got_repo_open(&repo, got_worktree_get_repo_path(worktree), + NULL, pack_fds); + if (error != NULL) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 0, + got_worktree_get_root_path(worktree)); + if (error) + goto done; + + error = check_merge_in_progress(worktree, repo); + if (error) + goto done; + + if (asprintf(&refname, "refs/heads/%s", branch_arg) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + + error = got_worktree_integrate_prepare(&fileindex, &branch_ref, + &base_branch_ref, worktree, refname, repo); + if (error) + goto done; + + refname = strdup(got_ref_get_name(branch_ref)); + if (refname == NULL) { + error = got_error_from_errno("strdup"); + got_worktree_integrate_abort(worktree, fileindex, repo, + branch_ref, base_branch_ref); + goto done; + } + base_refname = strdup(got_ref_get_name(base_branch_ref)); + if (base_refname == NULL) { + error = got_error_from_errno("strdup"); + got_worktree_integrate_abort(worktree, fileindex, repo, + branch_ref, base_branch_ref); + goto done; + } + if (strncmp(base_refname, "refs/heads/", 11) != 0) { + error = got_error(GOT_ERR_INTEGRATE_BRANCH); + got_worktree_integrate_abort(worktree, fileindex, repo, + branch_ref, base_branch_ref); + goto done; + } + + error = got_ref_resolve(&commit_id, repo, branch_ref); + if (error) + goto done; + + error = got_ref_resolve(&base_commit_id, repo, base_branch_ref); + if (error) + goto done; + + if (got_object_id_cmp(commit_id, base_commit_id) == 0) { + error = got_error_msg(GOT_ERR_SAME_BRANCH, + "specified branch has already been integrated"); + got_worktree_integrate_abort(worktree, fileindex, repo, + branch_ref, base_branch_ref); + goto done; + } + + error = check_linear_ancestry(commit_id, base_commit_id, 1, repo); + if (error) { + if (error->code == GOT_ERR_ANCESTRY) + error = got_error(GOT_ERR_REBASE_REQUIRED); + got_worktree_integrate_abort(worktree, fileindex, repo, + branch_ref, base_branch_ref); + goto done; + } + + memset(&upa, 0, sizeof(upa)); + error = got_worktree_integrate_continue(worktree, fileindex, repo, + branch_ref, base_branch_ref, update_progress, &upa, + check_cancelled, NULL); + if (error) + goto done; + + printf("Integrated %s into %s\n", refname, base_refname); + print_update_progress_stats(&upa); +done: + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (worktree) + got_worktree_close(worktree); + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + free(cwd); + free(base_commit_id); + free(commit_id); + free(refname); + free(base_refname); + return error; +} + +__dead static void +usage_merge(void) +{ + fprintf(stderr, "usage: %s merge [-aCcn] [branch]\n", getprogname()); + exit(1); +} + +static const struct got_error * +cmd_merge(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_worktree *worktree = NULL; + struct got_repository *repo = NULL; + struct got_fileindex *fileindex = NULL; + char *cwd = NULL, *id_str = NULL, *author = NULL; + char *gitconfig_path = NULL; + struct got_reference *branch = NULL, *wt_branch = NULL; + struct got_object_id *branch_tip = NULL, *yca_id = NULL; + struct got_object_id *wt_branch_tip = NULL; + int ch, merge_in_progress = 0, abort_merge = 0, continue_merge = 0; + int allow_conflict = 0, prefer_fast_forward = 1, interrupt_merge = 0; + struct got_update_progress_arg upa; + struct got_object_id *merge_commit_id = NULL; + char *branch_name = NULL; + int *pack_fds = NULL; + + memset(&upa, 0, sizeof(upa)); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "aCcMn")) != -1) { + switch (ch) { + case 'a': + abort_merge = 1; + break; + case 'C': + allow_conflict = 1; + break; + case 'c': + continue_merge = 1; + break; + case 'M': + prefer_fast_forward = 0; + break; + case 'n': + interrupt_merge = 1; + break; + default: + usage_merge(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (abort_merge) { + if (continue_merge) + option_conflict('a', 'c'); + if (!prefer_fast_forward) + option_conflict('a', 'M'); + if (interrupt_merge) + option_conflict('a', 'n'); + } else if (continue_merge) { + if (!prefer_fast_forward) + option_conflict('c', 'M'); + if (interrupt_merge) + option_conflict('c', 'n'); + } + if (allow_conflict) { + if (!continue_merge) + errx(1, "-C option requires -c"); + } + if (abort_merge || continue_merge) { + if (argc != 0) + usage_merge(); + } else if (argc != 1) + usage_merge(); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_open(&worktree, cwd); + if (error) { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, + "merge", cwd); + goto done; + } + + error = get_gitconfig_path(&gitconfig_path); + if (error) + goto done; + error = got_repo_open(&repo, + worktree ? got_worktree_get_repo_path(worktree) : cwd, + gitconfig_path, pack_fds); + if (error != NULL) + goto done; + + if (worktree != NULL) { + error = worktree_has_logmsg_ref("merge", worktree, repo); + if (error) + goto done; + } + + error = apply_unveil(got_repo_get_path(repo), 0, + worktree ? got_worktree_get_root_path(worktree) : NULL); + if (error) + goto done; + + error = check_rebase_or_histedit_in_progress(worktree); + if (error) + goto done; + + error = got_worktree_merge_in_progress(&merge_in_progress, worktree, + repo); + if (error) + goto done; + + if (merge_in_progress && !(abort_merge || continue_merge)) { + error = got_error(GOT_ERR_MERGE_BUSY); + goto done; + } + + if (!merge_in_progress && (abort_merge || continue_merge)) { + error = got_error(GOT_ERR_NOT_MERGING); + goto done; + } + + if (abort_merge) { + error = got_worktree_merge_continue(&branch_name, + &branch_tip, &fileindex, worktree, repo); + if (error) + goto done; + error = got_worktree_merge_abort(worktree, fileindex, repo, + abort_progress, &upa); + if (error) + goto done; + printf("Merge of %s aborted\n", branch_name); + goto done; /* nothing else to do */ + } + + if (strncmp(got_worktree_get_head_ref_name(worktree), + "refs/heads/", 11) != 0) { + error = got_error_fmt(GOT_ERR_COMMIT_BRANCH, + "work tree's current branch %s is outside the " + "\"refs/heads/\" reference namespace; " + "update -b required", + got_worktree_get_head_ref_name(worktree)); + goto done; + } + + error = get_author(&author, repo, worktree); + if (error) + goto done; + + error = got_ref_open(&wt_branch, repo, + got_worktree_get_head_ref_name(worktree), 0); + if (error) + goto done; + error = got_ref_resolve(&wt_branch_tip, repo, wt_branch); + if (error) + goto done; + + if (continue_merge) { + struct got_object_id *base_commit_id; + base_commit_id = got_worktree_get_base_commit_id(worktree); + if (got_object_id_cmp(wt_branch_tip, base_commit_id) != 0) { + error = got_error(GOT_ERR_MERGE_COMMIT_OUT_OF_DATE); + goto done; + } + error = got_worktree_merge_continue(&branch_name, + &branch_tip, &fileindex, worktree, repo); + if (error) + goto done; + } else { + error = got_ref_open(&branch, repo, argv[0], 0); + if (error != NULL) + goto done; + branch_name = strdup(got_ref_get_name(branch)); + if (branch_name == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + error = got_ref_resolve(&branch_tip, repo, branch); + if (error) + goto done; + } + + error = got_commit_graph_find_youngest_common_ancestor(&yca_id, + wt_branch_tip, branch_tip, 0, repo, + check_cancelled, NULL); + if (error && error->code != GOT_ERR_ANCESTRY) + goto done; + + if (!continue_merge) { + error = check_path_prefix(wt_branch_tip, branch_tip, + got_worktree_get_path_prefix(worktree), + GOT_ERR_MERGE_PATH, repo); + if (error) + goto done; + error = got_worktree_merge_prepare(&fileindex, worktree, repo); + if (error) + goto done; + if (prefer_fast_forward && yca_id && + got_object_id_cmp(wt_branch_tip, yca_id) == 0) { + struct got_pathlist_head paths; + if (interrupt_merge) { + error = got_error_fmt(GOT_ERR_BAD_OPTION, + "there are no changes to merge since %s " + "is already based on %s; merge cannot be " + "interrupted for amending; -n", + branch_name, got_ref_get_name(wt_branch)); + goto done; + } + printf("Forwarding %s to %s\n", + got_ref_get_name(wt_branch), branch_name); + error = got_ref_change_ref(wt_branch, branch_tip); + if (error) + goto done; + error = got_ref_write(wt_branch, repo); + if (error) + goto done; + error = got_worktree_set_base_commit_id(worktree, repo, + branch_tip); + if (error) + goto done; + TAILQ_INIT(&paths); + error = got_pathlist_append(&paths, "", NULL); + if (error) + goto done; + error = got_worktree_checkout_files(worktree, + &paths, repo, update_progress, &upa, + check_cancelled, NULL); + got_pathlist_free(&paths, GOT_PATHLIST_FREE_NONE); + if (error) + goto done; + if (upa.did_something) { + char *id_str; + error = got_object_id_str(&id_str, branch_tip); + if (error) + goto done; + printf("Updated to commit %s\n", id_str); + free(id_str); + } else + printf("Already up-to-date\n"); + print_update_progress_stats(&upa); + goto done; + } + error = got_worktree_merge_write_refs(worktree, branch, repo); + if (error) + goto done; + + error = got_worktree_merge_branch(worktree, fileindex, + yca_id, branch_tip, repo, update_progress, &upa, + check_cancelled, NULL); + if (error) + goto done; + print_merge_progress_stats(&upa); + if (!upa.did_something) { + error = got_worktree_merge_abort(worktree, fileindex, + repo, abort_progress, &upa); + if (error) + goto done; + printf("Already up-to-date\n"); + goto done; + } + } + + if (interrupt_merge) { + error = got_worktree_merge_postpone(worktree, fileindex); + if (error) + goto done; + printf("Merge of %s interrupted on request\n", branch_name); + } else if (upa.conflicts > 0 || upa.missing > 0 || + upa.not_deleted > 0 || upa.unversioned > 0) { + error = got_worktree_merge_postpone(worktree, fileindex); + if (error) + goto done; + if (upa.conflicts > 0 && upa.missing == 0 && + upa.not_deleted == 0 && upa.unversioned == 0) { + error = got_error_msg(GOT_ERR_CONFLICTS, + "conflicts must be resolved before merging " + "can continue"); + } else if (upa.conflicts > 0) { + error = got_error_msg(GOT_ERR_CONFLICTS, + "conflicts must be resolved before merging " + "can continue; changes destined for some " + "files were not yet merged and " + "should be merged manually if required before the " + "merge operation is continued"); + } else { + error = got_error_msg(GOT_ERR_CONFLICTS, + "changes destined for some " + "files were not yet merged and should be " + "merged manually if required before the " + "merge operation is continued"); + } + goto done; + } else { + error = got_worktree_merge_commit(&merge_commit_id, worktree, + fileindex, author, NULL, 1, branch_tip, branch_name, + allow_conflict, repo, continue_merge ? print_status : NULL, + NULL); + if (error) + goto done; + error = got_worktree_merge_complete(worktree, fileindex, repo); + if (error) + goto done; + error = got_object_id_str(&id_str, merge_commit_id); + if (error) + goto done; + printf("Merged %s into %s: %s\n", branch_name, + got_worktree_get_head_ref_name(worktree), + id_str); + + } +done: + free(gitconfig_path); + free(id_str); + free(merge_commit_id); + free(author); + free(branch_tip); + free(branch_name); + free(yca_id); + if (branch) + got_ref_close(branch); + if (wt_branch) + got_ref_close(wt_branch); + if (worktree) + got_worktree_close(worktree); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + return error; +} + +__dead static void +usage_stage(void) +{ + fprintf(stderr, "usage: %s stage [-lpS] [-F response-script] " + "[path ...]\n", getprogname()); + exit(1); +} + +static const struct got_error * +print_stage(void *arg, unsigned char status, unsigned char staged_status, + const char *path, struct got_object_id *blob_id, + struct got_object_id *staged_blob_id, struct got_object_id *commit_id, + int dirfd, const char *de_name) +{ + const struct got_error *err = NULL; + char *id_str = NULL; + + if (staged_status != GOT_STATUS_ADD && + staged_status != GOT_STATUS_MODIFY && + staged_status != GOT_STATUS_DELETE) + return NULL; + + if (staged_status == GOT_STATUS_ADD || + staged_status == GOT_STATUS_MODIFY) + err = got_object_id_str(&id_str, staged_blob_id); + else + err = got_object_id_str(&id_str, blob_id); + if (err) + return err; + + printf("%s %c %s\n", id_str, staged_status, path); + free(id_str); + return NULL; +} + +static const struct got_error * +cmd_stage(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + char *cwd = NULL; + struct got_pathlist_head paths; + int ch, list_stage = 0, pflag = 0, allow_bad_symlinks = 0; + FILE *patch_script_file = NULL; + const char *patch_script_path = NULL; + struct choose_patch_arg cpa; + int *pack_fds = NULL; + + TAILQ_INIT(&paths); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "F:lpS")) != -1) { + switch (ch) { + case 'F': + patch_script_path = optarg; + break; + case 'l': + list_stage = 1; + break; + case 'p': + pflag = 1; + break; + case 'S': + allow_bad_symlinks = 1; + break; + default: + usage_stage(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (list_stage && (pflag || patch_script_path)) + errx(1, "-l option cannot be used with other options"); + if (patch_script_path && !pflag) + errx(1, "-F option can only be used together with -p option"); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_open(&worktree, cwd); + if (error) { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, "stage", cwd); + goto done; + } + + error = got_repo_open(&repo, got_worktree_get_repo_path(worktree), + NULL, pack_fds); + if (error != NULL) + goto done; + + if (patch_script_path) { + patch_script_file = fopen(patch_script_path, "re"); + if (patch_script_file == NULL) { + error = got_error_from_errno2("fopen", + patch_script_path); + goto done; + } + } + error = apply_unveil(got_repo_get_path(repo), 0, + got_worktree_get_root_path(worktree)); + if (error) + goto done; + + error = check_merge_in_progress(worktree, repo); + if (error) + goto done; + + error = get_worktree_paths_from_argv(&paths, argc, argv, worktree); + if (error) + goto done; + + if (list_stage) + error = got_worktree_status(worktree, &paths, repo, 0, + print_stage, NULL, check_cancelled, NULL); + else { + cpa.patch_script_file = patch_script_file; + cpa.action = "stage"; + error = got_worktree_stage(worktree, &paths, + pflag ? NULL : print_status, NULL, + pflag ? choose_patch : NULL, &cpa, + allow_bad_symlinks, repo); + } +done: + if (patch_script_file && fclose(patch_script_file) == EOF && + error == NULL) + error = got_error_from_errno2("fclose", patch_script_path); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (worktree) + got_worktree_close(worktree); + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + got_pathlist_free(&paths, GOT_PATHLIST_FREE_PATH); + free(cwd); + return error; +} + +__dead static void +usage_unstage(void) +{ + fprintf(stderr, "usage: %s unstage [-p] [-F response-script] " + "[path ...]\n", getprogname()); + exit(1); +} + + +static const struct got_error * +cmd_unstage(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + char *cwd = NULL; + struct got_pathlist_head paths; + int ch, pflag = 0; + struct got_update_progress_arg upa; + FILE *patch_script_file = NULL; + const char *patch_script_path = NULL; + struct choose_patch_arg cpa; + int *pack_fds = NULL; + + TAILQ_INIT(&paths); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "F:p")) != -1) { + switch (ch) { + case 'F': + patch_script_path = optarg; + break; + case 'p': + pflag = 1; + break; + default: + usage_unstage(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (patch_script_path && !pflag) + errx(1, "-F option can only be used together with -p option"); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + error = got_worktree_open(&worktree, cwd); + if (error) { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, "unstage", cwd); + goto done; + } + + error = got_repo_open(&repo, got_worktree_get_repo_path(worktree), + NULL, pack_fds); + if (error != NULL) + goto done; + + if (patch_script_path) { + patch_script_file = fopen(patch_script_path, "re"); + if (patch_script_file == NULL) { + error = got_error_from_errno2("fopen", + patch_script_path); + goto done; + } + } + + error = apply_unveil(got_repo_get_path(repo), 0, + got_worktree_get_root_path(worktree)); + if (error) + goto done; + + error = get_worktree_paths_from_argv(&paths, argc, argv, worktree); + if (error) + goto done; + + cpa.patch_script_file = patch_script_file; + cpa.action = "unstage"; + memset(&upa, 0, sizeof(upa)); + error = got_worktree_unstage(worktree, &paths, update_progress, + &upa, pflag ? choose_patch : NULL, &cpa, repo); + if (!error) + print_merge_progress_stats(&upa); +done: + if (patch_script_file && fclose(patch_script_file) == EOF && + error == NULL) + error = got_error_from_errno2("fclose", patch_script_path); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (worktree) + got_worktree_close(worktree); + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + got_pathlist_free(&paths, GOT_PATHLIST_FREE_PATH); + free(cwd); + return error; +} + +__dead static void +usage_cat(void) +{ + fprintf(stderr, "usage: %s cat [-P] [-c commit] [-r repository-path] " + "arg ...\n", getprogname()); + exit(1); +} + +static const struct got_error * +cat_blob(struct got_object_id *id, struct got_repository *repo, FILE *outfile) +{ + const struct got_error *err; + struct got_blob_object *blob; + int fd = -1; + + fd = got_opentempfd(); + if (fd == -1) + return got_error_from_errno("got_opentempfd"); + + err = got_object_open_as_blob(&blob, repo, id, 8192, fd); + if (err) + goto done; + + err = got_object_blob_dump_to_file(NULL, NULL, NULL, outfile, blob); +done: + if (fd != -1 && close(fd) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (blob) + got_object_blob_close(blob); + return err; +} + +static const struct got_error * +cat_tree(struct got_object_id *id, struct got_repository *repo, FILE *outfile) +{ + const struct got_error *err; + struct got_tree_object *tree; + int nentries, i; + + err = got_object_open_as_tree(&tree, repo, id); + if (err) + return err; + + nentries = got_object_tree_get_nentries(tree); + for (i = 0; i < nentries; i++) { + struct got_tree_entry *te; + char *id_str; + if (sigint_received || sigpipe_received) + break; + te = got_object_tree_get_entry(tree, i); + err = got_object_id_str(&id_str, got_tree_entry_get_id(te)); + if (err) + break; + fprintf(outfile, "%s %.7o %s\n", id_str, + got_tree_entry_get_mode(te), + got_tree_entry_get_name(te)); + free(id_str); + } + + got_object_tree_close(tree); + return err; +} + +static const struct got_error * +cat_commit(struct got_object_id *id, struct got_repository *repo, FILE *outfile) +{ + const struct got_error *err; + struct got_commit_object *commit; + const struct got_object_id_queue *parent_ids; + struct got_object_qid *pid; + char *id_str = NULL; + const char *logmsg = NULL; + char gmtoff[6]; + + err = got_object_open_as_commit(&commit, repo, id); + if (err) + return err; + + err = got_object_id_str(&id_str, got_object_commit_get_tree_id(commit)); + if (err) + goto done; + + fprintf(outfile, "%s%s\n", GOT_COMMIT_LABEL_TREE, id_str); + parent_ids = got_object_commit_get_parent_ids(commit); + fprintf(outfile, "numparents %d\n", + got_object_commit_get_nparents(commit)); + STAILQ_FOREACH(pid, parent_ids, entry) { + char *pid_str; + err = got_object_id_str(&pid_str, &pid->id); + if (err) + goto done; + fprintf(outfile, "%s%s\n", GOT_COMMIT_LABEL_PARENT, pid_str); + free(pid_str); + } + got_date_format_gmtoff(gmtoff, sizeof(gmtoff), + got_object_commit_get_author_gmtoff(commit)); + fprintf(outfile, "%s%s %lld %s\n", GOT_COMMIT_LABEL_AUTHOR, + got_object_commit_get_author(commit), + (long long)got_object_commit_get_author_time(commit), + gmtoff); + + got_date_format_gmtoff(gmtoff, sizeof(gmtoff), + got_object_commit_get_committer_gmtoff(commit)); + fprintf(outfile, "%s%s %lld %s\n", GOT_COMMIT_LABEL_COMMITTER, + got_object_commit_get_committer(commit), + (long long)got_object_commit_get_committer_time(commit), + gmtoff); + + logmsg = got_object_commit_get_logmsg_raw(commit); + fprintf(outfile, "messagelen %zd\n", strlen(logmsg)); + fprintf(outfile, "%s", logmsg); +done: + free(id_str); + got_object_commit_close(commit); + return err; +} + +static const struct got_error * +cat_tag(struct got_object_id *id, struct got_repository *repo, FILE *outfile) +{ + const struct got_error *err; + struct got_tag_object *tag; + char *id_str = NULL; + const char *tagmsg = NULL; + char gmtoff[6]; + + err = got_object_open_as_tag(&tag, repo, id); + if (err) + return err; + + err = got_object_id_str(&id_str, got_object_tag_get_object_id(tag)); + if (err) + goto done; + + fprintf(outfile, "%s%s\n", GOT_TAG_LABEL_OBJECT, id_str); + + switch (got_object_tag_get_object_type(tag)) { + case GOT_OBJ_TYPE_BLOB: + fprintf(outfile, "%s%s\n", GOT_TAG_LABEL_TYPE, + GOT_OBJ_LABEL_BLOB); + break; + case GOT_OBJ_TYPE_TREE: + fprintf(outfile, "%s%s\n", GOT_TAG_LABEL_TYPE, + GOT_OBJ_LABEL_TREE); + break; + case GOT_OBJ_TYPE_COMMIT: + fprintf(outfile, "%s%s\n", GOT_TAG_LABEL_TYPE, + GOT_OBJ_LABEL_COMMIT); + break; + case GOT_OBJ_TYPE_TAG: + fprintf(outfile, "%s%s\n", GOT_TAG_LABEL_TYPE, + GOT_OBJ_LABEL_TAG); + break; + default: + break; + } + + fprintf(outfile, "%s%s\n", GOT_TAG_LABEL_TAG, + got_object_tag_get_name(tag)); + + got_date_format_gmtoff(gmtoff, sizeof(gmtoff), + got_object_tag_get_tagger_gmtoff(tag)); + fprintf(outfile, "%s%s %lld %s\n", GOT_TAG_LABEL_TAGGER, + got_object_tag_get_tagger(tag), + (long long)got_object_tag_get_tagger_time(tag), + gmtoff); + + tagmsg = got_object_tag_get_message(tag); + fprintf(outfile, "messagelen %zd\n", strlen(tagmsg)); + fprintf(outfile, "%s", tagmsg); +done: + free(id_str); + got_object_tag_close(tag); + return err; +} + +static const struct got_error * +cmd_cat(int argc, char *argv[]) +{ + const struct got_error *error; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + char *cwd = NULL, *repo_path = NULL, *label = NULL; + const char *commit_id_str = NULL; + struct got_object_id *id = NULL, *commit_id = NULL; + struct got_commit_object *commit = NULL; + int ch, obj_type, i, force_path = 0; + struct got_reflist_head refs; + int *pack_fds = NULL; + + TAILQ_INIT(&refs); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath flock proc exec sendfd unveil", + NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "c:Pr:")) != -1) { + switch (ch) { + case 'c': + commit_id_str = optarg; + break; + case 'P': + force_path = 1; + break; + case 'r': + repo_path = realpath(optarg, NULL); + if (repo_path == NULL) + return got_error_from_errno2("realpath", + optarg); + got_path_strip_trailing_slashes(repo_path); + break; + default: + usage_cat(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_repo_pack_fds_open(&pack_fds); + if (error != NULL) + goto done; + + if (repo_path == NULL) { + error = got_worktree_open(&worktree, cwd); + if (error && error->code != GOT_ERR_NOT_WORKTREE) + goto done; + if (worktree) { + repo_path = strdup( + got_worktree_get_repo_path(worktree)); + if (repo_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + + /* Release work tree lock. */ + got_worktree_close(worktree); + worktree = NULL; + } + } + + if (repo_path == NULL) { + repo_path = strdup(cwd); + if (repo_path == NULL) + return got_error_from_errno("strdup"); + } + + error = got_repo_open(&repo, repo_path, NULL, pack_fds); + free(repo_path); + if (error != NULL) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 1, NULL); + if (error) + goto done; + + error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, NULL); + if (error) + goto done; + + if (commit_id_str == NULL) + commit_id_str = GOT_REF_HEAD; + error = got_repo_match_object_id(&commit_id, NULL, + commit_id_str, GOT_OBJ_TYPE_COMMIT, &refs, repo); + if (error) + goto done; + + error = got_object_open_as_commit(&commit, repo, commit_id); + if (error) + goto done; + + for (i = 0; i < argc; i++) { + if (force_path) { + error = got_object_id_by_path(&id, repo, commit, + argv[i]); + if (error) + break; + } else { + error = got_repo_match_object_id(&id, &label, argv[i], + GOT_OBJ_TYPE_ANY, NULL /* do not resolve tags */, + repo); + if (error) { + if (error->code != GOT_ERR_BAD_OBJ_ID_STR && + error->code != GOT_ERR_NOT_REF) + break; + error = got_object_id_by_path(&id, repo, + commit, argv[i]); + if (error) + break; + } + } + + error = got_object_get_type(&obj_type, repo, id); + if (error) + break; + + switch (obj_type) { + case GOT_OBJ_TYPE_BLOB: + error = cat_blob(id, repo, stdout); + break; + case GOT_OBJ_TYPE_TREE: + error = cat_tree(id, repo, stdout); + break; + case GOT_OBJ_TYPE_COMMIT: + error = cat_commit(id, repo, stdout); + break; + case GOT_OBJ_TYPE_TAG: + error = cat_tag(id, repo, stdout); + break; + default: + error = got_error(GOT_ERR_OBJ_TYPE); + break; + } + if (error) + break; + free(label); + label = NULL; + free(id); + id = NULL; + } +done: + free(label); + free(id); + free(commit_id); + if (commit) + got_object_commit_close(commit); + if (worktree) + got_worktree_close(worktree); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (pack_fds) { + const struct got_error *pack_err = + got_repo_pack_fds_close(pack_fds); + if (error == NULL) + error = pack_err; + } + + got_ref_list_free(&refs); + return error; +} + +__dead static void +usage_info(void) +{ + fprintf(stderr, "usage: %s info [path ...]\n", + getprogname()); + exit(1); +} + +static const struct got_error * +print_path_info(void *arg, const char *path, mode_t mode, time_t mtime, + struct got_object_id *blob_id, struct got_object_id *staged_blob_id, + struct got_object_id *commit_id) +{ + const struct got_error *err = NULL; + char *id_str = NULL; + char datebuf[128]; + struct tm mytm, *tm; + struct got_pathlist_head *paths = arg; + struct got_pathlist_entry *pe; + + /* + * Clear error indication from any of the path arguments which + * would cause this file index entry to be displayed. + */ + TAILQ_FOREACH(pe, paths, entry) { + if (got_path_cmp(path, pe->path, strlen(path), + pe->path_len) == 0 || + got_path_is_child(path, pe->path, pe->path_len)) + pe->data = NULL; /* no error */ + } + + printf(GOT_COMMIT_SEP_STR); + if (S_ISLNK(mode)) + printf("symlink: %s\n", path); + else if (S_ISREG(mode)) { + printf("file: %s\n", path); + printf("mode: %o\n", mode & (S_IRWXU | S_IRWXG | S_IRWXO)); + } else if (S_ISDIR(mode)) + printf("directory: %s\n", path); + else + printf("something: %s\n", path); + + tm = localtime_r(&mtime, &mytm); + if (tm == NULL) + return NULL; + if (strftime(datebuf, sizeof(datebuf), "%c %Z", tm) == 0) + return got_error(GOT_ERR_NO_SPACE); + printf("timestamp: %s\n", datebuf); + + if (blob_id) { + err = got_object_id_str(&id_str, blob_id); + if (err) + return err; + printf("based on blob: %s\n", id_str); + free(id_str); + } + + if (staged_blob_id) { + err = got_object_id_str(&id_str, staged_blob_id); + if (err) + return err; + printf("based on staged blob: %s\n", id_str); + free(id_str); + } + + if (commit_id) { + err = got_object_id_str(&id_str, commit_id); + if (err) + return err; + printf("based on commit: %s\n", id_str); + free(id_str); + } + + return NULL; +} + +static const struct got_error * +cmd_info(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_worktree *worktree = NULL; + char *cwd = NULL, *id_str = NULL; + struct got_pathlist_head paths; + char *uuidstr = NULL; + int ch, show_files = 0; + + TAILQ_INIT(&paths); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath flock proc exec sendfd unveil", + NULL) == -1) + err(1, "pledge"); +#endif + + while ((ch = getopt(argc, argv, "")) != -1) { + switch (ch) { + default: + usage_info(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_worktree_open(&worktree, cwd); + if (error) { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, "info", cwd); + goto done; + } + +#ifndef PROFILE + /* Remove "wpath cpath proc exec sendfd" promises. */ + if (pledge("stdio rpath flock unveil", NULL) == -1) + err(1, "pledge"); +#endif + error = apply_unveil(NULL, 0, got_worktree_get_root_path(worktree)); + if (error) + goto done; + + if (argc >= 1) { + error = get_worktree_paths_from_argv(&paths, argc, argv, + worktree); + if (error) + goto done; + show_files = 1; + } + + error = got_object_id_str(&id_str, + got_worktree_get_base_commit_id(worktree)); + if (error) + goto done; + + error = got_worktree_get_uuid(&uuidstr, worktree); + if (error) + goto done; + + printf("work tree: %s\n", got_worktree_get_root_path(worktree)); + printf("work tree base commit: %s\n", id_str); + printf("work tree path prefix: %s\n", + got_worktree_get_path_prefix(worktree)); + printf("work tree branch reference: %s\n", + got_worktree_get_head_ref_name(worktree)); + printf("work tree UUID: %s\n", uuidstr); + printf("repository: %s\n", got_worktree_get_repo_path(worktree)); + + if (show_files) { + struct got_pathlist_entry *pe; + TAILQ_FOREACH(pe, &paths, entry) { + if (pe->path_len == 0) + continue; + /* + * Assume this path will fail. This will be corrected + * in print_path_info() in case the path does suceeed. + */ + pe->data = (void *)got_error(GOT_ERR_BAD_PATH); + } + error = got_worktree_path_info(worktree, &paths, + print_path_info, &paths, check_cancelled, NULL); + if (error) + goto done; + TAILQ_FOREACH(pe, &paths, entry) { + if (pe->data != NULL) { + const struct got_error *perr; + + perr = pe->data; + error = got_error_fmt(perr->code, "%s", + pe->path); + break; + } + } + } +done: + if (worktree) + got_worktree_close(worktree); + got_pathlist_free(&paths, GOT_PATHLIST_FREE_PATH); + free(cwd); + free(id_str); + free(uuidstr); + return error; +}