--- /dev/null +++ /sys/lib/git/common.rc Fri Jun 19 00:00:00 2026 @@ -0,0 +1,122 @@ +nl=' +' + +fn die{ + >[1=2] echo $0: $* + exit $"* +} + +fn usage{ + >[1=2] echo -n 'usage:' $usage + exit 'usage' +} + +fn subst { + awk ' + BEGIN{ARGC=0} + {sub(ARGV[1], ARGV[2]); print} + ' $* +} + +fn drop { + awk ' + BEGIN{ARGC=0} + { + if(index($0, ARGV[1]) == 1) + $0=substr($0, length(ARGV[1])+1) + print + } + ' $* +} + +fn mergeperm { + if(~ $1 /dev/null && cmp $2 $3>/dev/null) + status=gone + if not if (~ $3 /dev/null && cmp $1 $2>/dev/null) + status=gone + if not { + mergedperms='-x' + if(test -x $2){ + if(test -x $1 -a -x $3) + mergedperms='+x' + } + if not{ + if(test -x $1 -o -x $3) + mergedperms='+x' + } + status=() + } +} + +fn whoami{ + name=`$nl{git/conf user.name} + email=`$nl{git/conf user.email} + if(test -f /adm/keys.who){ + if(~ $name '') + name=`$nl{awk -F'|' '$1=="'$user'" {x=$3} END{print x}' $tmp) + echo merge needed: $out >[1=2] + mv $tmp $out + git/add $out + chmod $mergedperms $out + } + if not { + rm -f $tmp $out + echo R NOQID 0 $out >> $gitroot/.git/INDEX9 + } +}} + +fn gitup{ + gitroot=`$nl{git/conf -r >[2]/dev/null} + if(~ $#gitroot 0) + die 'not a git repository' + gitfs=$gitroot/.git/fs + gitrel=`$nl{pwd | drop $gitroot | sed 's@^/@@'} + if(~ $#gitrel 0) + gitrel='.' + if(! cd $gitroot) + die cd $gitroot: no repo there + startfs=() + mkdir -p $gitfs + if(! test -e $gitfs/ctl) + startfs=true + if(! grep -s '^repo '$gitroot'$' $gitfs/ctl >[2]/dev/null) + startfs=true + if(~ $#startfs 1) + git/fs >[1=2] + if not + status='' +} --- /dev/null +++ /sys/man/1/git Fri Jun 19 00:00:00 2026 @@ -0,0 +1,695 @@ +.TH GIT 1 +.SH NAME +git, git/conf, git/query, git/walk, git/clone, git/branch, +git/commit, git/diff, git/init, git/log, git/merge, git/push, +git/pull, git/rm, git/serve, git/hist \- manage git repositories +.SH SYNOPSIS +.B git/add +[ +.B -r +] +.I path ... +.PP +.B git/branch +[ +.B -armns +] +[ +.B -b +.I base +] +.I newbranch +.PP +.B git/clone +[ +.I remote +[ +.I local +] +] +.PP +.B git/commit +[ +.B -rep +] +[ +.B -m +.I msg +] +[ +.I file ... +] +.PP +.B git/compat +.PP +.B git/conf +[ +.B -r +] +[ +.B -f +.I file +] +.I keys ... +.PP +.B git/diff +[ +.B -c +.I branch +] +[ +.B -su +] +[ +.I file ... +] +.PP +.B git/export +[ +.I commits ... +] +.PP +.B git/import +[ +.B -n +] +[ +.I commits ... +] +.PP +.B git/init +[ +.B -b +] +[ +.B -u +.I upstream +] +[ +.I dir +] +.PP +.B git/log +[ +.B -c +.I commit +.B | -e +.I expr +] +[ +.B -n +.I count +] +[ +.B -s +] +[ +.I files ... +] +.PP +.B git/merge +.I theirs +.PP +.B git/rebase +[ +.B -s src +] +[ +.B -n +] +.I onto +.PP +.B git/pull +[ +.B -fq +] +[ +.B -u +.I upstream +] +.PP +.B git/push +[ +.B -af +] +[ +.B -u +.I upstream +] +[ +.B -b +.I branch +] +[ +.B -r +.I branch +] +.PP +.B git/query +[ +.B -pcr +] +.I query +.PP +.B git/revert +[ +.B -c +.I commit +] +.I file ... +.PP +.B git/rm +.I path ... +.PP +.B git/serve +[ +.B -w +] +[ +.B -r +.I path +] +.PP +.B git/walk +[ +.B -qc +] +[ +.B -b +.I branch +] +[ +.B -f +.I filters +] +[ +.B -r +.I rel +] +[ +.I file ... +] +.PP +.B git/hist +[ +.B -n +.I count +] +.I files... +.SH DESCRIPTION +Git is a distributed version control system. +This means that each repository contains a full copy of the history. +This history is then synced between computers as needed. +.PP +These programs provide tools to manage and interoperate with +repositories hosted in git. +.SH CONCEPTS +Git stores snapshots of the working directory. +Files can either be in a tracked or untracked state. +Untracked files will be ignored. +.PP +This history is stored in the +.B .git +directory. +This suite of +.I git +tools provides a file interface to the +.B .git +directory mounted on +.BR $repo/.git/fs . +Modifications to the repository are done directly to the +.B .git +directory, and are reflected in the file system interface. +This allows for easy scripting, without excessive complexity +in the file API. +.SH COMMANDS +.I Git/init +is used to create a new git repository, with no code or commits. +The repository is created in the current directory by default. +Passing a directory name will cause the repository to be created +there instead. +Passing the +.B -b +option will cause the repository to be initialized as a bare repository. +Passing the +.B -u +.I upstream +option will cause the upstream to be configured to +.I upstream. +.PP +.I Git/clone +will take an existing repository, served over either the +.B git:// +or +.B ssh:// +protocols. +The first argument is the repository to clone. +The second argument, optionally, specifies the location to clone into. +If not specified, the repository will be cloned into the last path component +of the clone source, with the +.B .git +stripped off if present. +.PP +.I Git/push +is used to push the current changes to a remote repository. +When no arguments are provided, the remote repository is taken from +the origin configured in +.BR .git/config , +and only the changes on the current branch are pushed. +When passed the +.B -a +option, all branches are pushed. +When passed the +.B -u +.I upstream +option, the changes are pushed to +.I upstream +instead of the configured origin. +When given the +.B -r +option, the branch is deleted from origin, instead of updated. +.PP +.I Git/revert +restores the named files from HEAD. When passed the +.B -c +flag, restores files from +the named commit. +.PP +.I Git/pull +behaves in a similar manner to +.IR git/push , +however it gets changes from +the upstream repository. +After fetching, it checks out the changes into the working directory. +When passed the +.B -f +option, the update of the working copy is suppressed. +When passed the +.B -q +option, the listing of changes is silenced. +When passed the +.B -u +.I upstream +option, the changes are pulled from +.I upstream +instead of the configured origin. +.PP +.I Git/serve +serves repositories using the +.B git:// +protocol over stdin. +By default, it serves them read-only. +The +.B -w +flag, it allows pushing into repositories. +The +.B -r +.I path +flag serves repositories relative to +.IR path . +.PP +.I Git/fs +serves a file system on +.BR $repo/.git/fs . +For full documentation, see +.IR gitfs (4) +.PP +.I Git/add +adds a file to the list of tracked files. When passed the +.B -r +flag, the file is removed from the list of tracked files. +The copy of the file in the repository is left untouched. +.PP +.I Git/rm +is an alias for +.I git/add +.BR -r . +.PP +.I Git/commit +creates a new commit consisting of all changes to the specified files. +By default, +.I $editor +is opened to prepare the commit message. +If +.I $editor +is undefined +.IR hold (1) +is used. +The +.B -m +flag supplies the commit message directly. +The +.B -r +flag revises the contents of the previous commit, reusing the message. +The +.B -e +flag opens an editor to finalize the commit message, regardless of +whether or not it was specified explicitly or reused. +To amend a commit message, +.B -r +can be used in conjunction with +.B -m +or +.BR -e . +The +.B -p +option commits only portions of each file. +Individual hunks are selected interactively. +.PP +.I Git/branch +is used to list or switch branches. +When invoked with no arguments, it lists the current branch. +To list all branches, pass the +.B -a +option. +To switch between branches, pass a branch name. +When passed the +.B -n +option, the branch will be created, overwriting existing branch. +When passed the +.B -b +.I base +option, the branch created is based off of +.I base +instead of +.BR HEAD . +When passed the +.B -s +option, the branch is created but the files are not checked out. +When passed the +.B -r +option, the branch is removed. +.PP +When switching branches, +.I git/branch +will refuse to clobber +modifications. +Passing the +.B -m +option will cause it to attempt to merge the changes between +the branches. +.PP +.I Git/log +shows a history of the current branch. +When passed a list of files, only commits affecting +those files are shown. +The +.B -c +.I commit +option logs starting from the provided commit, instead of +.BR HEAD . +The +.B -s +option shows a summary of the commit, instead of the full message. +The +.B -n +.I count +option stops printing messages after +.I count +messages. +The +.B -e +.I expr +option shows commits matching the query expression provided. +The expression is in the syntax of +.IR git/query . +.PP +.I Git/diff +shows the differences between the currently checked out code and +the +.B HEAD +commit. +When passed the +.B -c +.I base +option, the diff is computed against +.I base +instead of +.BR HEAD . +When passed the +.B -s +option, only the file statuses are +printed. +When passed the +.B -u +option, untracked files are listed. +The +.B -u +option +implies +.BR -s . +.PP +.I Git/export +exports a list of commits in a format that +.I git/import +can apply. +If multiple patches are generated, +they are separated with a ⑨ character. +.PP +.I Git/import +imports a commit with message, author, and +date information. +When passed the +.B -n +option, +it applies the contents of the commit without committing to the branch. +Git/import can accept multiple patches on stdin. +When multiple patches are supplied, each patch must be separated with a ⑨ character. +.PP +.I Git/merge +takes two branches and merges them filewise using +.I merge3 +(see +.IR diff (1)). +The next commit made will be a merge commit. +.PP +.I Git/rebase +prints the commands to move commits from one +branch onto another, using +.I git/export +and +.I git/import. +The +.B -n +option is passed directly to +.I git/import. +.PP +.I Git/conf +is a tool for querying the git configuration. +The configuration key is provided as a dotted string. +Spaces are accepted. +For example, to find the URL of the origin +repository, one might pass +.BR 'remote\ \C'"'origin\C'"'.url' . +When given the +.B -r +option, the root of the current repository is printed. +.PP +.I Git/query +takes an expression describing a commit, or set of commits, +and resolves it to a list of commits. +The +.B -r +option reverses the order of the commit list. +With the +.B -p +option, instead of printing the commit hashes, the full +path to their +.I git/fs +path is printed. With the +.B -c +option, the query must resolve to two commits. +The blobs +that have changed in the commits are printed. +.PP +.I Git/walk +shows the status of files in the repository. +It prints a list of paths prefixed with the status character. +It exits with a non-empty status if the repository is not clean. +The +.B -c +option suppresses the status, so only the paths are printed. +The +.B -q +option suppresses all output. +The +.B -r +option causes paths to be printed relative to the supplied directory +.IR rel . +The +.B -f +option filters files by status, and only matching items are printed. +By default, the filters are +.BR TRMA . +.PP +The status characters are as follows: +.TP +T +Tracked, not modified since last commit. +.TP +M +Modified since last commit. +.TP +R +This file will be gone in the next commit. +.TP +A +This file will be present in the next commit. +.TP +U +This file is not tracked by git. +.PP +.I Git/compat +spawns an rc subshell with a compatibility stub in +.BR $path . +This compatibility stub provides enough of the Unix +.I git +commands to run tools like +.I go get +but not much more. +.PP +.I Git/hist +provides the diffs of specific +.I files +in reverse chronological order. +The +.B -n +flag may be used to limit the output to the last +.I count +commits. +.SH REF SYNTAX +Refs are specified with a simple query syntax. +A bare hash always evaluates to itself. +Ref names are resolved to their hashes. +The +.B a ^ +suffix operator finds the parent of a commit. +The +.B a b @ +suffix operator finds the common ancestor of the previous two commits. +The +.B a .. b +or +.B a : b +operator finds all commits between +.B a +and +.BR b . +Between is defined as the set of all commits which are reachable from +.B b +but not reachable from +.BR a . +.SH PROTOCOLS +Git supports URL schemes of the format +.IB transport :// dial / repo . +The +.I transport +portion specifies the protocol to use. +If the +.I transport +portion is omitted, then the transport used is +.BR ssh . +The +.I dial +portion is either a Plan 9 dial string, or a conventional +.IB host : port +pair. +For the +.B ssh +protocol, it may also include a +.IB user @ +prefix. +.I repo +portion is the path of the repository on the server. +.PP +The supported transports are +.BR ssh:// , +.BR git:// , +.BR hjgit:// , +.BR gits:// , +.BR http:// , +and +.BR https:// . +Two of these are specific to git9: +.B gits:// +and +.BR hjgit:// . +Both are the +.B git:// +protocol, tunneled over TLS. +.B Hjgit:// +authenticates with the server using Plan 9 authentication, +using +.IR tlsclient\ -a . +Any of these protocol names may be prefixed with +.BR git+ , +for copy-paste compatibility with Unix git. +.SH EXAMPLES +In order to create a new repository, run +.I git/init: +.IP +.EX +git/init myrepo +.EE +.PP +To clone an existing repository from a git server, run: +.IP +.EX +git/clone https://github.com/Harvey-OS/harvey +cd harvey +# edit files +git/commit foo.c +git/push +.EE +.PP +To set a user and email for commits, run: +.IP +.EX +% mkdir $home/lib/git +% >$home/lib/git/config echo ' +[user] + name = Ori Bernstein + email = ori@eigenstate.org +\&' +.EE +.SH FILES +.TP +.B $repo/.git +The full git repository. +.TP +.B $repo/.git/config +The configuration file for a repository. +.TP +.B $home/lib/git/config +The user-wide configuration for git. +The contents of this file are used as fallbacks for the per-repository config. +.TP +.B /sys/lib/git/config +The system-wide configuration for git. +The contents of this file are used as fallbacks for the per-user config. +.SH SEE ALSO +.IR diff (1), +.IR patch (1), +.IR replica (1), +.IR gitfs (4) +.SH BUGS +Repositories with submodules are effectively read-only. +.PP +There are some missing commands, features, and tools. +.PP +.I Git/compat +only works within a git repository. +.PP +Sharing git repositories between Plan 9 and Linux leads to indexes going out of sync. +To work around this, reset the state of the tracked files on Plan 9 with +.I git/add: +.IP +.EX +git/add `{@{git/fs && git/walk -c}} +.EE +.PP +And on Linux, with: +.IP +.EX +git checkout HEAD -- $(find . -type f) +.EE --- /dev/null +++ /sys/man/4/gitfs Fri Jun 19 00:00:00 2026 @@ -0,0 +1,104 @@ +.TH GITFS 4 +.SH NAME +git/fs \- git file server + +.SH SYNOPSIS + +git/fs +[ +.B -d +] +[ +.B -m +.I mtpt +] + +.SH DESCRIPTION + +.PP +.I Git/fs +serves a file system interface to a git repository in the +current directory. +This file system provides a read-only view into the repository contents. +By default, it is mounted on +.B $repo/.git/fs. +It does not cache mutable data, so any changes to the git repository will immediately be reflected in +.IR git/fs . + +.PP +.I Git/fs +serves a few levels of hierarchy. +The top level contains the following files and directories: + +.TP +.B branch +Exposes branches. Branches are aliases for commit objects. + +.TP +.B object +Exposes all objects in the git repository. +Objects may be commits, trees, or blobs. + +.TP +.B HEAD +This is an alias for the current commit. + +.PP +Commits are also represented as small hierarchies. They contain +the following files: + +.TP +.B author +This is the author of the commit. +The contents of this file are free-form text, but conventionally +they take the form +.B Full Name + +.TP +.B hash +The commit id of the current branch + +.TP +.B msg +The full text of the commit message. + +.TP +.B parent +The list of parent commit ids of the current commit. +One parent is listed per line. + +.TP +.B tree +A directory containing the tree associated with the +commit. +The timestamp of the files contained within this +hierarchy are the same as the date of the commit. + +.PP +Trees are presented as directory listings, and blobs +as files. + +.SH FILES +.TP +.B .git +The git repository being mounted. + +.SH SOURCE +.TP +.B /sys/src/cmd/git/fs.c + +.SH "SEE ALSO" +.IR git (1) +.IR hg (1) +.IR hgfs (4) + +.SH BUGS +Symlinks are only partially supported. +Symlinks are treated as regular files when reading. +Modifying symlinks is unsupported. + +.PP +There is no way to inspect the raw objects. This is +a feature that would be useful for debugging. + + --- /dev/null +++ /sys/src/cmd/git/add Fri Jun 19 00:00:00 2026 @@ -0,0 +1,19 @@ +#!/bin/rc -e +rfork ne +. /sys/lib/git/common.rc + +gitup + +flagfmt='r:remove'; args='file ...' +eval `''{aux/getflags $*} || exec aux/usage + +s=A +if(~ $remove 1) + s=R +if(~ $#* 0) + exec aux/usage + +paths=`$nl{cleanname -d $gitrel $* | drop $gitroot} +walk -f ./$paths | grep -v '^(./)?.git/' | \ + sed 's/^/'$s' NOQID 0 /' >> .git/INDEX9 +exit '' --- /dev/null +++ /sys/src/cmd/git/branch Fri Jun 19 00:00:00 2026 @@ -0,0 +1,136 @@ +#!/bin/rc -e +rfork en +. /sys/lib/git/common.rc + +gitup + +flagfmt='a:listall, b:baseref ref, r:remove, n:newbr, s:stay, m:merge, M:nomod' +args='[branch]' +eval `''{aux/getflags $*} || exec aux/usage + +modified=() +deleted=() +failed=() +if(~ $#* 0){ + if(~ $#listall 0) + awk '$1=="branch"{print $2}' < $gitfs/ctl + if not + cd .git/refs/ && walk -f heads remotes | sort + exit +} +if(! ~ $#* 1) + exec aux/usage + +branch=$1 +if(~ $branch refs/heads/*) + new=$name +if not if(~ $branch heads/*) + new=refs/$branch +if not + new=refs/heads/$branch + +# where are we now? +orig=`{git/query HEAD} +origbranch=refs/`{awk '$1=="branch"{print $2}' < $gitfs/ctl} + +# if we're switching branches to a branch that doesn't exist, +# but we have a remote branch that exists upstream, create a +# new head to mirror it. +if(~ $#newbr 0){ + if(! ~ $#baseref 0) + die update would clobber $branch with $baseref + if(! test -e .git/$new){ + baseref=`$nl{echo -n $new | sed s@refs/heads/@refs/remotes/origin/@} + if(! base=`{git/query $baseref}) + exit 'bad ref' + } +} + +# figure out where we want the new branch to point to +if (~ $#baseref 1) + base=`{git/query $baseref} || exit 'bad ref' +if not if(~ $#newbr 0) + base=`{git/query $new} || exit 'bad ref' +if not + base=`{git/query HEAD} || exit 'bad ref' + +modified=`$nl{git/query -c HEAD $base | grep '^[^-]' | subst '^..'} +deleted=`$nl{git/query -c HEAD $base | grep '^-' | subst '^..'} + +# if we remove the current branch without switching, bad things happen +if(~ $remove 1 && ~ $origbranch $new) + die 'cannot remove current branch' +# if we're not merging, don't clobber existing changes. +if(~ $#merge 0 && ~ $#remove 0){ + if(! ~ $#modified 0 || ! ~ $#deleted 0){ + git/walk -fRMA $modified $deleted || + die 'uncommitted changes would be clobbered' + } +} +if(~ $remove 1){ + rm -f .git/$new + echo 'removed branch' $new + exit +} +commit=`{git/query $base} || die 'branch does not exist:' $base +basedir=`{git/query -p $base} +dirtypaths=() +if(! ~ $#modified 0 || ! ~ $#deleted 0) + dirtypaths=`$nl{git/walk -cfRMA $modified $deleted} + +>>[3].git/INDEX9{ + for(d in $deleted){ + if(! test -d $d){ + rm -f $d + echo R NOQID 0 $d >>[1=3] + } + } + for(m in $modified $deleted) if(! ~ $m $dirtypaths){ + d=`$nl{basename -d $m} + mkdir -p $d + # Modifications can turn a file into + # a directory, or vice versa, so we + # need to delete and copy the files + # over. + a=dir + b=dir + if(test -f $m) + a=file + if(test -f $basedir/tree/$m) + b=file + if(! ~ $a $b){ + rm -rf $m + echo R NOQID 0 $m >>[1=3] + } + if(~ $b file){ + echo T NOQID 0 $m >>[1=3] + cp -x -- $basedir/tree/$m $m || {echo cp failed: $m; failed=1} + touch -c $m + } + } +} +modified=() +deleted=() + +for(ours in $dirtypaths){ + common=$gitfs/object/$orig/tree/$ours + theirs=$gitfs/object/$base/tree/$ours + merge1 $ours $ours $common $theirs + st=$status + if(! ~ $st '') + >[1=2] echo merge failed $ours: $st +} +dirtypaths=() + +if(~ $new */*) + mkdir -p .git/`{basename -d $new} +echo $commit > .git/$new +if(! ~ $#stay 0) + exit +if(~ $failed 1){ + echo 'pull failed: fix errors and try again' >[1=2] + exit nopull +} +echo ref: $new > .git/HEAD +echo $new: `{git/query $new} +exit '' --- /dev/null +++ /sys/src/cmd/git/clone Fri Jun 19 00:00:00 2026 @@ -0,0 +1,116 @@ +#!/bin/rc +rfork en +. /sys/lib/git/common.rc + +flagfmt='d:debug, b:branch branch'; args='remote [local]' +eval `''{aux/getflags $*} || exec aux/usage +if(~ $debug 1) + debug=(-d) + +remote=$1 +local=$2 + +if(~ $#remote 0) + exec aux/usage +if(~ $#local 0) + local=`$nl{basename `$nl{echo $remote | sed 's@.*/([^/]+)/*$@\1@'} .git} +if(~ $#branch 1) + branchflag=(-b $branch) + +if(test -e $local && ~ `{ls $local | sed 1q | wc -l} 1) + die 'destination already exists:' $local + +fn clone{ + flag +e + mkdir -p $local/.git + mkdir -p $local/.git/fs + mkdir -p $local/.git/objects/pack/ + mkdir -p $local/.git/refs/heads/ + + cd $local + + >>.git/config { + echo '[remote "origin"]' + echo ' url='$remote + } + {git/get $debug $branchflag $remote >[2=3] | awk ' + BEGIN{ + headref="" + if(ENVIRON["branch"] != "") + headref="refs/remotes/origin/"ENVIRON["branch"] + headhash="" + } + /^symref / && headref == "" { + if($2 == "HEAD"){ + gsub("^refs/heads", "refs/remotes/origin", $3) + gsub("^refs/tags", "refs/remotes/origin/tags", $3) + } + } + /^remote /{ + if($2=="HEAD"){ + headhash=$3 + }else if(match($2, "^refs/(heads|tags)/")){ + gsub("^refs/heads", "refs/remotes/origin", $2) + if($2 == headref || (headref == "" && $3 == headhash)) + headref=$2 + outfile = ".git/" $2 + outdir = outfile + gsub("/?[^/]*/?$", "", outdir) + system("mkdir -p " outdir) + print $3 > outfile + close(outfile) + } + } + END{ + if(headref != ""){ + remote = headref; + refdir = headref; + gsub("/?[^/]*/?$", "", refdir) + gsub("^refs/remotes/origin", "refs/heads", headref) + localdir = ".git/" headref + sub("/[^/]*$", "", localdir) + system("mkdir -p " localdir) + system("cp .git/" remote " .git/" headref) + print "ref: " headref > ".git/HEAD" + }else if(headhash != ""){ + print "warning: detached head "headhash > "/fd/2" + print headhash > ".git/HEAD" + } + } + '} |[3] tr '\x0d' '\x0a' >[1=2] || die 'could not clone repository' + + tree=.git/fs/HEAD/tree + lbranch=`{git/branch} + rbranch=`{echo $lbranch | subst 'heads' 'remotes/origin'} + echo checking out repository... + if(test -f .git/refs/$rbranch){ + mkdir -p `{basename -d .git/refs/$lbranch} + cp .git/refs/$rbranch .git/refs/$lbranch + git/fs + @ {builtin cd $tree && tar cif /fd/1 .} | @ {tar xf /fd/0} \ + || die 'checkout failed:' $status + {for(f in `$nl{cd $tree && walk -f}) + echo 'T NOQID 0 '$f} > .git/INDEX9 + } + if not{ + echo no default branch >[1=2] + echo check out your code with git/branch >[1=2] + } +} + +fn sigint { + echo cancelled clone $remote: cleaning $local >[1=2] + unmount $local/.git/fs >[2]/dev/null + rm -rf $local + exit interrupted +} + +@{clone} +st=$status +if(! ~ $st ''){ + echo failed to clone $remote: cleaning $local >[1=2] + unmount $local/.git/fs >[2]/dev/null + rm -rf $local + exit $st +} +exit '' --- /dev/null +++ /sys/src/cmd/git/commit Fri Jun 19 00:00:00 2026 @@ -0,0 +1,197 @@ +#!/bin/rc -e +rfork ne +. /sys/lib/git/common.rc + +fn findbranch{ + branch=`{git/branch} + if(test -e $gitfs/branch/$branch/tree){ + refpath=.git/refs/$branch + initial=false + } + if not if(test -e $gitfs/object/$branch/tree){ + refpath=.git/HEAD + initial=false + } + if not if(! test -e $gitfs/HEAD/tree){ + refpath=.git/refs/$branch + initial=true + } + if not + die 'invalid branch:' $branch +} + +# Remove commentary lines. +# Remove leading and trailing empty lines. +# Combine consecutive empty lines between paragraphs. +# Remove trailing spaces from lines. +# Ensure there's trailing newline. +fn cleanmsg{ + awk ' + /^[ ]*#/ {next} + /^[ ]*$/ {empty = 1; next} + + wet && empty {printf "\n"} + {wet = 1; empty = 0} + {sub(/[ ]+$/, ""); print $0} + ' +} + +fn editmsg{ + if(! test -s $msgfile.tmp){ + >$msgfile.tmp { + echo '# Author:' $name '<'$email'>' + echo '#' + for(p in $parents) + echo '# parent:' $p + git/walk -fAMR $files | subst '^' '# ' + echo '#' + echo '# Commit message:' + } + edit=1 + } + if(! ~ $#edit 0){ + giteditor=`{git/conf core.editor} + if(~ $#editor 0) + editor=$giteditor + if(~ $#editor 0) + editor=hold + $editor $msgfile.tmp + } + cleanmsg < $msgfile.tmp > $msgfile + if(! test -s $msgfile) + die 'empty commit message' +} + +fn parents{ + if(! ~ $#revise 0) + parents=`{cat $gitfs/HEAD/parent} + if not if(test -f .git/merge-parents) + parents=`{cat .git/merge-parents | sort | uniq} + if not if(~ $initial true) + parents=() + if not + parents=`{git/query $branch} +} + +fn commit{ + msg=`''{cat $msgfile} + if(! ~ $#parents 0) + pflags='-p'^$parents + hash=`{git/save -n $"name -e $"email -m $"msg $pflags $files || die $status} +} + +fn update{ + mkdir -p `{basename -d $refpath} + # Paranoia: let's not mangle the repo. + if(~ $#hash 0) + die 'botched commit' + rm -f .git/merge-parents + echo $branch: $hash + echo $hash > $refpath + for(f in $files){ + if(! test -e $f && ! test -e .git/object/$hash/tree/$f) + echo R NOQID 0 $f >> .git/INDEX9 + if not + echo T NOQID 0 $f >> .git/INDEX9 + } +} + +fn filterdiff{ + awk ' + function dump(){ + if(n == 0) + return + + print "--- a/"f >>"/dev/cons" + print "+++ b/"f >>"/dev/cons" + for(i = 0; i < n; i++) + print lines[i] >>"/dev/cons" + printf("Commit this hunk?[n] ") >>"/dev/cons" + getline ans < "/dev/cons" + if(ans ~ /[yY]/) + for(i = 0; i < n; i++) + print lines[i] + n = 0 + } + /^---/{ next } + /^\+\+\+/{ + f = substr($0, 5) + print "--- "f + print "+++ "f + n = 0 + next + } + /^@@/{ dump() } + { lines[n++] = $0 } + END{ dump() } + ' +} + +fn pickhunks{ + p=$wdir/patch + if(test -f .git/fs/HEAD/tree/$i) + #<[12]/fd/0 >[13]/fd/1 { diff -u .git/fs/HEAD/tree/$1 $1 | filterdiff >>$p } + { diff -u .git/fs/HEAD/tree/$1 $1 | filterdiff >>$p } || status='' + if not + diff -u /dev/null $1 >>$p || status='' +} + +fn apply{ + t=$wdir/tree/ + p=$wdir/patch + ln=`{wc -l $p} + if(test $ln(1) -lt 3) + die 'no hunks seleted' + f=`{patch -n $p} + mkdir $t + @{cd .git/fs/HEAD/tree; tar c $f >[2]/dev/null} | @{cd $t; tar xT} + for(i in $f){ + d=`{basename -d $i} + if(! ~ $d .){ + mkdir -p $t^$d + bind -bc $t^$d $d + } + } + bind -bc $t . + patch <$p >/dev/null || die 'failed partial patch: '^$status +} + +gitup + +flagfmt='m:msg message, r:revise, e:edit, p:partial'; args='[file ...]' +eval `''{aux/getflags $*} || exec aux/usage + +wdir=/mnt/git +ramfs -m $wdir +msgfile=$wdir/git-msg.$pid +if(~ $#msg 1) + echo $msg >$msgfile.tmp +if not if(~ $#revise 1){ + msg=1 + echo revising commit `{cat $gitfs/HEAD/hash} + cat $gitfs/HEAD/msg >$msgfile.tmp +} + +files=() +if(test -f .git/merge-parents) + files=`$nl{git/query -c `{cat .git/merge-parents} | sed 's/^..//'} +if(! ~ $#* 0) + files=($files `$nl{git/walk -fRMA -c `$nl{cleanname -d $gitrel $*}}) +if(~ $status '' || ~ $#files 0 && ! test -f .git/merge-parents && ~ $#revise 0) + die 'nothing to commit' +if(~ $#partial 1){ + for(i in $files) + pickhunks $i + apply +} + +@{ + flag e + + whoami + findbranch + parents + editmsg + commit + update +} || die 'could not commit:' $status +exit '' --- /dev/null +++ /sys/src/cmd/git/compat Fri Jun 19 00:00:00 2026 @@ -0,0 +1,228 @@ +#!/bin/rc + +rfork e + +opts=() +args=() +nl=' +' + +fn cmd_init{ + while(! ~ $#* 0){ + switch($1){ + case --bare + # ignore + case -- + # go likes to use these + case -* + die unknown command init $* + case * + args=($args $1) + } + shift + } + git/init $opts $args +} + +fn cmd_clone{ + branch=() + while( ! ~ $#* 0){ + switch($1){ + case -b + branch=$2 + shift + case -- + # go likes to use these + case -* + die unknown command clone $* + case * + args=($args $1) + } + shift + } + git/clone $opts $args + if(~ $#branch 1) + git/branch -n -b $1 origin/$1 +} + +fn cmd_pull{ + if(~ $1 -*) + die unknown options for pull $* + git/pull +} + +fn cmd_fetch{ + while(~ $#* 0){ + switch($1){ + case --all + opts=($opts -a) + case -f + opts=($opts -u $2) + shift + case -- + # go likes to use these + case -* + die unknown command clone $* + case * + args=($args $1) + } + shift + } + git/pull -f $opts +} + + +fn cmd_checkout{ + if(~ $1 -*) + die unknown command pull $* + if(~ $#* 0) + die git checkout branch + git/branch $b +} + +fn cmd_submodule { + if(test -f .gitmodules) + die 'submodules unsupported' +} + +fn cmd_rev-parse{ + while(~ $1 -*){ + switch($1){ + case --git-dir + echo $gitroot/.git + shift + case --abbrev-ref + echo `{dcmd git9/branch | sed s@^heads/@@g} + shift + case * + die unknown option $opt + } + shift + } +} + +fn cmd_show-ref{ + if(~ $1 -*) + die unknown command pull $* + filter=cat + if(~ $#* 0) + filter=cat + if not + filter='-e(^|/)'^$*^'$' + for(b in `$nl{cd $gitroot/.git/refs/ && walk -f}) + echo `{cat $gitroot/.git/refs/$b} refs/$b +} + +fn cmd_rev-parse{ + switch($1){ + case --git-dir + echo `{git/conf -r}^/.git + case * + die 'unknown rev-parse '$* + } +} + +fn cmd_remote{ + if({! ~ $#* 3 && ! ~ $#* 4} || ! ~ $1 add) + die unimplemented remote cmd $* + name=$2 + url=$3 + if(~ $3 '--') + url=$4 + >>$gitroot/.git/config{ + echo '[remote "'$name'"]' + echo ' url='$url + } +} + +fn cmd_log{ + count=() + format='' + while(~ $1 -*){ + switch($1){ + case --format + format=$2 + shift + case '--format='* + format=`{echo $1 | sed 's/--format=//g'} + case -n + count=-n$2 + shift + case -n* + count=$1 + case * + dprint option $opt + } + shift + } + @{cd $gitroot && git/fs} + switch($format){ + case '' + git/log $count + case '%H:%ct' + for(c in `{git/log -s $count| awk '{print $1}'}) + echo $c:`{mtime $gitroot/.git/fs/object/$c/msg} + case '%h %cd' + for(c in `{git/log -s $count| awk '{print $1}'}) + echo $c `{date `{mtime $gitroot/.git/fs/object/$c/msg}} + } + +} + +fn cmd_show{ + cmd_log -n1 $* +} + +fn cmd_ls-remote{ + if(~ $1 -q) + shift + remote=`$nl{git/conf 'remote "'$1'".url'} + if(~ $#remote 0) + remote=$1 + git/get -l $remote | awk '/^remote/{print $3"\t"$2}' +} + +fn cmd_version{ + echo git version 2.2.0 +} + +fn cmd_status{ + echo +} + +fn usage{ + echo 'git ' >[1=2] + exit usage +} + +fn die { + >[1=2] echo git $_cmdname: $* + exit $_cmdname: $* +} + +_cmdname=$1 +if(~ $0 *compat){ + ramfs -m /n/gitcompat + touch /n/gitcompat/git + bind $0 /n/gitcompat/git + path=( /n/gitcompat $path ) + exec rc +} + +if(~ $#gitcompatdebug 1) + echo running $* >>/tmp/gitlog + +if(~ $1 -c) + shift 2 +if(~ $1 -c*) + shift 1 +if(! test -f '/env/fn#cmd_'$1) + die git $1: commmand not implemented +if(! ~ $1 init && ! ~ $1 clone) + gitroot=`{git/conf -r} || die repo + +if(~ $#gitcompatdebug 1) + cmd_$1 $*(2-) | tee >>/tmp/gitlog +if not + cmd_$1 $*(2-) +exit '' --- /dev/null +++ /sys/src/cmd/git/conf.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,103 @@ +#include +#include +#include + +#include "git.h" + +int findroot; +int showall; +int nfile; +char *file[32]; + +static int +showconf(char *cfg, char *sect, char *key) +{ + char *ln, *p; + Biobuf *f; + int foundsect, nsect, nkey, found; + + if((f = Bopen(cfg, OREAD)) == nil) + return 0; + + found = 0; + nsect = sect ? strlen(sect) : 0; + nkey = strlen(key); + foundsect = (sect == nil); + while((ln = Brdstr(f, '\n', 1)) != nil){ + p = strip(ln); + if(*p == '[' && sect){ + foundsect = strncmp(sect, ln, nsect) == 0; + }else if(foundsect && strncmp(p, key, nkey) == 0){ + p = strip(p + nkey); + if(*p != '=') + continue; + p = strip(p + 1); + print("%s\n", p); + found = 1; + if(!showall){ + free(ln); + goto done; + } + } + free(ln); + } +done: + return found; +} + + +void +usage(void) +{ + fprint(2, "usage: %s [-f file] [-r] keys..\n", argv0); + fprint(2, "\t-f: use file 'file' (default: .git/config)\n"); + fprint(2, "\t r: print repository root\n"); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char repo[512], *p, *s; + int i, j, nrel; + + ARGBEGIN{ + case 'r': findroot++; break; + case 'a': showall++; break; + default: usage(); break; + case 'f': + if(nfile == nelem(file)) + sysfatal("too many configs"); + file[nfile++]=EARGF(usage()); + break; + }ARGEND; + + gitinit(repo, sizeof(repo), &nrel); + if(findroot){ + print("%s\n", repo); + exits(nil); + } + if(nfile == 0){ + if((file[nfile++] = smprint("%s/.git/config", repo)) == nil) + sysfatal("smprint: %r"); + if((p = getenv("home")) != nil) + if((file[nfile++] = smprint("%s/lib/git/config", p)) == nil) + sysfatal("smprint: %r"); + file[nfile++] = "/sys/lib/git/config"; + } + + for(i = 0; i < argc; i++){ + if((p = strchr(argv[i], '.')) == nil){ + s = nil; + p = argv[i]; + }else{ + *p = 0; + p++; + s = smprint("[%s]", argv[i]); + } + for(j = 0; j < nfile; j++) + if(showconf(file[j], s, p)) + break; + } + exits(nil); +} --- /dev/null +++ /sys/src/cmd/git/delta.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,218 @@ +#include +#include + +#include "git.h" + +enum { + Minchunk = 128, + Maxchunk = 8192, + Splitmask = (1<<8)-1, +}; + +static u32int geartab[] = { + 0x67ed26b7, 0x32da500c, 0x53d0fee0, 0xce387dc7, 0xcd406d90, 0x2e83a4d4, 0x9fc9a38d, 0xb67259dc, + 0xca6b1722, 0x6d2ea08c, 0x235cea2e, 0x3149bb5f, 0x1beda787, 0x2a6b77d5, 0x2f22d9ac, 0x91fc0544, + 0xe413acfa, 0x5a30ff7a, 0xad6fdde0, 0x444fd0f5, 0x7ad87864, 0x58c5ff05, 0x8d2ec336, 0x2371f853, + 0x550f8572, 0x6aa448dd, 0x7c9ddbcf, 0x95221e14, 0x2a82ec33, 0xcbec5a78, 0xc6795a0d, 0x243995b7, + 0x1c909a2f, 0x4fded51c, 0x635d334b, 0x0e2b9999, 0x2702968d, 0x856de1d5, 0x3325d60e, 0xeb6a7502, + 0xec2a9844, 0x0905835a, 0xa1820375, 0xa4be5cab, 0x96a6c058, 0x2c2ccd70, 0xba40fce3, 0xd794c46b, + 0x8fbae83e, 0xc3aa7899, 0x3d3ff8ed, 0xa0d42b5b, 0x571c0c97, 0xd2811516, 0xf7e7b96c, 0x4fd2fcbd, + 0xe2fdec94, 0x282cc436, 0x78e8e95c, 0x80a3b613, 0xcfbee20c, 0xd4a32d1c, 0x2a12ff13, 0x6af82936, + 0xe5630258, 0x8efa6a98, 0x294fb2d1, 0xdeb57086, 0x5f0fddb3, 0xeceda7ce, 0x4c87305f, 0x3a6d3307, + 0xe22d2942, 0x9d060217, 0x1e42ed02, 0xb6f63b52, 0x4367f39f, 0x055cf262, 0x03a461b2, 0x5ef9e382, + 0x386bc03a, 0x2a1e79c7, 0xf1a0058b, 0xd4d2dea9, 0x56baf37d, 0x5daff6cc, 0xf03a951d, 0xaef7de45, + 0xa8f4581e, 0x3960b555, 0xffbfff6d, 0xbe702a23, 0x8f5b6d6f, 0x061739fb, 0x98696f47, 0x3fd596d4, + 0x151eac6b, 0xa9fcc4f5, 0x69181a12, 0x3ac5a107, 0xb5198fe7, 0x96bcb1da, 0x1b5ddf8e, 0xc757d650, + 0x65865c3a, 0x8fc0a41a, 0x87435536, 0x99eda6f2, 0x41874794, 0x29cff4e8, 0xb70efd9a, 0x3103f6e7, + 0x84d2453b, 0x15a450bd, 0x74f49af1, 0x60f664b1, 0xa1c86935, 0xfdafbce1, 0xe36353e3, 0x5d9ba739, + 0xbc0559ba, 0x708b0054, 0xd41d808c, 0xb2f31723, 0x9027c41f, 0xf136d165, 0xb5374b12, 0x9420a6ac, + 0x273958b6, 0xe6c2fad0, 0xebdc1f21, 0xfb33af8b, 0xc71c25cd, 0xe9a2d8e5, 0xbeb38a50, 0xbceb7cc2, + 0x4e4e73f0, 0xcd6c251d, 0xde4c032c, 0x4b04ac30, 0x725b8b21, 0x4eb8c33b, 0x20d07b75, 0x0567aa63, + 0xb56b2bb7, 0xc1f5fd3a, 0xcafd35ca, 0x470dd4da, 0xfe4f94cd, 0xfb8de424, 0xe8dbcf40, 0xfe50a37a, + 0x62db5b5d, 0xf32f4ab6, 0x2c4a8a51, 0x18473dc0, 0xfe0cbb6e, 0xfe399efd, 0xdf34ecc9, 0x6ccd5055, + 0x46097073, 0x139135c2, 0x721c76f6, 0x1c6a94b4, 0x6eee014d, 0x8a508e02, 0x3da538f5, 0x280d394f, + 0x5248a0c4, 0x3ce94c6c, 0x9a71ad3a, 0x8493dd05, 0xe43f0ab6, 0x18e4ed42, 0x6c5c0e09, 0x42b06ec9, + 0x8d330343, 0xa45b6f59, 0x2a573c0c, 0xd7fd3de6, 0xeedeab68, 0x5c84dafc, 0xbbd1b1a8, 0xa3ce1ad1, + 0x85b70bed, 0xb6add07f, 0xa531309c, 0x8f8ab852, 0x564de332, 0xeac9ed0c, 0x73da402c, 0x3ec52761, + 0x43af2f4d, 0xd6ff45c8, 0x4c367462, 0xd553bd6a, 0x44724855, 0x3b2aa728, 0x56e5eb65, 0xeaf16173, + 0x33fa42ff, 0xd714bb5d, 0xfbd0a3b9, 0xaf517134, 0x9416c8cd, 0x534cf94f, 0x548947c2, 0x34193569, + 0x32f4389a, 0xfe7028bc, 0xed73b1ed, 0x9db95770, 0x468e3922, 0x0440c3cd, 0x60059a62, 0x33504562, + 0x2b229fbd, 0x5174dca5, 0xf7028752, 0xd63c6aa8, 0x31276f38, 0x0646721c, 0xb0191da8, 0xe00e6de0, + 0x9eac1a6e, 0x9f7628a5, 0xed6c06ea, 0x0bb8af15, 0xf119fb12, 0x38693c1c, 0x732bc0fe, 0x84953275, + 0xb82ec888, 0x33a4f1b3, 0x3099835e, 0x028a8782, 0x5fdd51d7, 0xc6c717b3, 0xb06caf71, 0x17c8c111, + 0x61bad754, 0x9fd03061, 0xe09df1af, 0x3bc9eb73, 0x85878413, 0x9889aaf2, 0x3f5a9e46, 0x42c9f01f, + 0x9984a4f4, 0xd5de43cc, 0xd294daed, 0xbecba2d2, 0xf1f6e72c, 0x5551128a, 0x83af87e2, 0x6f0342ba, +}; + +static u64int +hash(void *p, int n) +{ + return murmurhash2(p, n); +} + +static void +addblk(Dtab *dt, void *buf, int len, int off, u64int h) +{ + int i, sz, probe; + Dblock *db; + + probe = h % dt->sz; + while(dt->b[probe].buf != nil){ + if(len == dt->b[probe].len && memcmp(buf, dt->b[probe].buf, len) == 0) + return; + probe = (probe + 1) % dt->sz; + } + assert(dt->b[probe].buf == nil); + dt->b[probe].buf = buf; + dt->b[probe].len = len; + dt->b[probe].off = off; + dt->b[probe].hash = h; + dt->nb++; + if(dt->sz < 2*dt->nb){ + sz = dt->sz; + db = dt->b; + dt->sz *= 2; + dt->nb = 0; + dt->b = eamalloc(dt->sz, sizeof(Dblock)); + for(i = 0; i < sz; i++) + if(db[i].buf != nil) + addblk(dt, db[i].buf, db[i].len, db[i].off, db[i].hash); + free(db); + } +} + +static Dblock* +lookup(Dtab *dt, uchar *p, int n) +{ + int probe; + u64int h; + + h = hash(p, n); + for(probe = h % dt->sz; dt->b[probe].buf != nil; probe = (probe + 1) % dt->sz){ + if(dt->b[probe].hash != h) + continue; + if(n != dt->b[probe].len) + continue; + if(memcmp(p, dt->b[probe].buf, n) != 0) + continue; + return &dt->b[probe]; + } + return nil; +} + +static int +nextblk(uchar *s, uchar *e) +{ + u32int gh; + uchar *p; + + if((e - s) < Minchunk) + return e - s; + p = s + Minchunk; + if((e - s) > Maxchunk) + e = s + Maxchunk; + gh = 0; + while(p != e){ + gh = (gh<<1) + geartab[*p++]; + if((gh & Splitmask) == 0) + break; + } + return p - s; +} + +void +dtinit(Dtab *dt, Object *obj) +{ + uchar *s, *e; + u64int h; + vlong n, o; + + o = 0; + s = (uchar*)obj->data; + e = s + obj->size; + dt->o = ref(obj); + dt->nb = 0; + dt->sz = 128; + dt->b = eamalloc(dt->sz, sizeof(Dblock)); + dt->base = (uchar*)obj->data; + dt->nbase = obj->size; + while(s != e){ + n = nextblk(s, e); + h = hash(s, n); + addblk(dt, s, n, o, h); + s += n; + o += n; + } +} + +void +dtclear(Dtab *dt) +{ + unref(dt->o); + free(dt->b); +} + +static int +emitdelta(Delta **pd, int *nd, int cpy, int off, int len) +{ + Delta *d; + + *nd += 1; + *pd = earealloc(*pd, *nd, sizeof(Delta)); + d = &(*pd)[*nd - 1]; + d->cpy = cpy; + d->off = off; + d->len = len; + return len; +} + +static int +stretch(Dtab *dt, Dblock *b, uchar *s, uchar *e, int n) +{ + uchar *p0, *p, *q, *eb; + + if(b == nil) + return n; + p = s + n; + q = dt->base + b->off + n; + p0 = p; + eb = dt->base + dt->nbase; + while(1){ + if(p == e || q == eb) + break; + if(n+(p-p0) >= (1<<24)-1) + break; + if(*p != *q) + break; + p++; + q++; + } + return n + (p - p0); +} + +Delta* +deltify(Object *obj, Dtab *dt, int *pnd) +{ + Delta *d; + Dblock *b; + uchar *s, *e; + vlong n, o; + + o = 0; + d = nil; + s = (uchar*)obj->data; + e = s + obj->size; + *pnd = 0; + while(s != e){ + n = nextblk(s, e); + b = lookup(dt, s, n); + n = stretch(dt, b, s, e, n); + if(b != nil) + emitdelta(&d, pnd, 1, b->off, n); + else + emitdelta(&d, pnd, 0, o, n); + s += n; + o += n; + } + return d; +} --- /dev/null +++ /sys/src/cmd/git/diff Fri Jun 19 00:00:00 2026 @@ -0,0 +1,48 @@ +#!/bin/rc +rfork ne +. /sys/lib/git/common.rc + +gitup + +flagfmt='c:commit branch, s:summarize, u:uncommitted'; args='[file ...]' +eval `''{aux/getflags $*} || exec aux/usage + +bparam=(-b $commit) +if(~ $#commit 0){ + commit=HEAD + cparam=() + bparam=() +} + +files=() +filt=MAR +if(~ $#uncommitted 1) + filt=MARU +if(! ~ $#* 0) + files=`{cleanname -d $gitrel $*} + +branch=`{git/query -p $commit} +if(~ $summarize 1 || ~ $uncommitted 1){ + git/walk $bparam -r$gitrel -f$filt $cparam $files + exit +} + +showed=() +mntgen /mnt/scratch +bind $branch/tree/ /mnt/scratch/a +bind . /mnt/scratch/b +for(f in `$nl{git/walk $bparam -c -f$filt $cparam $files}){ + if(~ $#showed 0){ + echo diff `{git/query $commit} uncommitted + showed=1 + } + cd /mnt/scratch + a=a/$f + b=b/$f + if(! test -f a/$f) + a=/dev/null + if(! test -f b/$f) + b=/dev/null + ape/diff -u $a $b +} +exit '' --- /dev/null +++ /sys/src/cmd/git/export Fri Jun 19 00:00:00 2026 @@ -0,0 +1,90 @@ +#!/bin/rc +rfork ne +. /sys/lib/git/common.rc + +patchname=/tmp/git.patchname.$pid +patchfile=/tmp/git.patchfile.$pid +fn sigexit{ + rm -f $patchname $patchfile +} + +gitup + +flagfmt='o:patchdir patchdir'; args='[query]' +eval `''{aux/getflags $*} || exec aux/usage + +if(~ $#patchdir 1 && ! test -d $patchdir) + mkdir -p $patchdir + +q=$* +if(~ $#q 0) + q=HEAD +commits=`{git/query $q || die $status} +n=1 +m=$#commits + + +# sleazy hack: we want to run +# under rfork m for the web ui, +# so don't error if we can't mount +mntgen /mnt/scratch >[2]/dev/null || status='' +for(c in $commits){ + cp=`{git/query -p $c} + pp=`{git/query -p $c'~'} + fc=`$nl{git/query -c $c~ $c | sed 's/^..//'} + + @{ + rfork n + cd /mnt/scratch + if(test -d $pp/tree) + bind $pp/tree a + if(test -d $cp/tree) + bind $cp/tree b + + echo From: `{cat $cp/author} + echo Date: `{date -uf'WW, DD MMM YYYY hh:mm:ss Z' `{walk -em $cp/author}} + <$cp/msg awk ' + NR == 1 { + n = ENVIRON["n"] + m = ENVIRON["m"] + msg=$0 + if(m > 1) + patch = sprintf("[PATCH %d/%d]", n, m) + else + patch = "[PATCH]" + printf "Subject: %s %s\n\n", patch, msg + + gsub("^[ ]|[ ]$", "", msg) + gsub("[^a-zA-Z0-9_]+", "-", msg) + printf "%.4d-%s.patch", n, msg >ENVIRON["patchname"] + next + } + { + print + }' + echo '---' + echo diff `{basename $pp} `{basename $cp} + for(f in $fc){ + a=a/$f + if(! test -f $a) + a=/dev/null + b=b/$f + if(! test -f $b) + b=/dev/null + diff -ur $a $b + } + echo '-- ' + echo '⑨' + } >$patchfile + if(~ $#patchdir 0){ + cat $patchfile + ! ~ $n $m && echo + } + if not{ + f=$patchdir/`{cat $patchname} + mv $patchfile $f + echo $f + } + n=`{echo $n + 1 | bc} +} +exit '' --- /dev/null +++ /sys/src/cmd/git/extra/gitls Fri Jun 19 00:00:00 2026 @@ -0,0 +1,203 @@ +#!/bin/rc -e + +cd $1 +shift + +rfork ne +nl=' +' + +fn htcat { + sed ' + s/&/\&/g; + s//\>/g; + s/"/\"/g; + s/''/\'/g + ' $* +} + +fn resolveref { + if(~ $refname HEAD) + echo $refname + if not if(test -d $gitfs/branch/$refname/tree) + echo branch/$refname + if not if(test -d $gitfs/object/$refname/tree) + echo object/$refname + if not + status='bad ref' +} + +fn repons { + mntgen + mntgen /mnt/mnt + bind /bin /mnt/bin + bind /tmp /mnt/tmp + bind -c /env /mnt/env + bind $1 /mnt/$repo + bind /mnt / + cd /mnt/$repo + git/fs + rfork m +} + + +fn prelude { + echo ' + + + + + + + + git webls + + + ' +} + +switch($1){ +case 'tar' + repo=$2 + refname=$3 + @{ + repons $2 + if(! ref=`{resolveref $refname}){ + echo 'invalid ref '$refname'' + exit + } + bind $gitfs/$ref/tree /mnt/$repo + cd /mnt + tar cz $repo + } + +case 'list' + rfork m + prelude + echo '

Repos

+
' + for(repo in `$nl{ls}){ + if(test -e $repo/.git/webpublish){ + echo '
'$repo'
' + echo '
' + if(test -f $repo/.git/desc) + htcat $repo/.git/desc + if not + echo 'code some guy wrote' + echo '
' + } + } + echo '
' + +case 'info' + repo=$2 + repodir=/mnt/$repo/.git + refname=$3 + @{ + repons $repo + if(! ref=`{resolveref $refname}){ + echo 'invalid ref '$refname'' + exit + } + cd $gitfs/$ref/tree + hash=`{cat $gitfs/$ref/hash} + + prelude $repo $ref $repo + echo '

Git: '$repo'

+

'$repo' @ '$hash' +

'
+	htcat $gitfs/object/$hash/msg
+	echo '	
+

Code

+

+ clone: git://orib.dev/'$repo', gits://orib.dev/'$repo'
+ push: hjgit://orib.dev/'$repo'
+ tar: snap.tar.gz
' + if(test -f $repodir/contact) + echo 'patches to: '^`$nl{cat $repodir/contact}^'
+

+
'
+	for(f in `$nl{ls}){
+		url=`$nl{echo -n $f/f.html | urlencode}
+		fname=`$nl{echo -n $f | htcat}
+		echo ''$fname''
+	}		
+	echo '
+

About This Repo

+
'
+	if(test -f $repodir/README)
+		htcat $repodir/README
+	if not if(test -f README)
+		htcat README
+	if not if (test -f README.md)
+		htcat README.md
+	if not if(test -f $repodir/desc)
+		htcat $repodir/desc
+	if not
+		echo 'this repo has no description'
+	echo '
+		
+ + + ' + } + +case 'view' + repo=$2 + repodir=/mnt/$repo/.git + refname=$3 + file=$4 + @{ + repons $repo + if(! ref=`{resolveref $refname}){ + echo 'invalid ref '$refname'' + exit + } + cd $gitfs/$ref/tree + if(~ $file '') + file='.' + hash=`{cat $gitfs/$ref/hash} + + prelude + echo '

Git: '$repo'

+

'$repo' @ '$hash' +

'
+	if(test -f $file){
+		htcat $file
+	}
+	if not if(test -d $file){
+		cd $file
+		for(f in `$nl{ls}){
+			url=`$nl{echo -n $f/f.html | urlencode}
+			fname=`$nl{echo -n $f | htcat}
+			echo ''$fname''
+		}
+	}
+	echo '	
+ + ' + } +} --- /dev/null +++ /sys/src/cmd/git/extra/gitrules Fri Jun 19 00:00:00 2026 @@ -0,0 +1,4 @@ +/repos.html /bin/gitls /usr/git list +/([^'/]+)/([^'/]+)/info.html /bin/gitls /usr/git info '\1' '\2' +/([^'/]+)/([^'/]+)/snap.tar.gz /bin/gitls /usr/git tar '\1' '\2' +/([^'/]+)/([^'/]+)/(([^']+)/)?f.html /bin/gitls /usr/git view '\1' '\2' '\4' --- /dev/null +++ /sys/src/cmd/git/fs.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,928 @@ +#include +#include +#include +#include +#include +#include <9p.h> + +#include "git.h" + +enum { + Qroot, + Qhead, + Qbranch, + Qcommit, + Qmsg, + Qparent, + Qtree, + Qhash, + Qauthor, + Qcommitter, + Qobject, + Qctl, + Qmax, + Internal=1<<7, +}; + +typedef struct Gitaux Gitaux; +typedef struct Crumb Crumb; +typedef struct Cache Cache; +typedef struct Uqid Uqid; +struct Crumb { + char *name; + Object *obj; + Qid qid; + int mode; + vlong mtime; +}; + +struct Gitaux { + int ncrumb; + Crumb *crumb; + char *refpath; + int qdir; + + /* For listing object dir */ + Objlist *ols; + Object *olslast; +}; + +struct Uqid { + vlong uqid; + + vlong ppath; + vlong oid; + int t; + int idx; +}; + +struct Cache { + Uqid *cache; + int n; + int max; +}; + +char *qroot[] = { + "HEAD", + "branch", + "object", + "ctl", +}; + +#define Eperm "permission denied" +#define Eexist "file does not exist" +#define Enotdir "is not a directory" +#define E2long "path too long" +#define Enodir "not a directory" +#define Erepo "unable to read repo" +#define Eimpl "not implemented" +#define Egreg "wat" +#define Ebadobj "invalid object" +#define Ename "invalid path element" + +char gitdir[512]; +char *username; +char *groupname; +char *mntpt = ".git/fs"; +char **branches = nil; +Cache uqidcache[512]; +vlong nextqid = Qmax; + +static Object* walklink(Gitaux *, char *, int, int, int*); + +vlong +qpath(Crumb *p, int idx, vlong id, vlong t) +{ + int h, i; + vlong pp; + Cache *c; + Uqid *u; + + pp = p ? p->qid.path : 0; + h = (pp*333 + id*7 + t) & (nelem(uqidcache) - 1); + c = &uqidcache[h]; + u = c->cache; + for(i=0; i n ; i++){ + if(u->ppath == pp && u->oid == id && u->t == t && u->idx == idx) + return (u->uqid << 8) | t; + u++; + } + if(c->n == c->max){ + c->max += c->max/2 + 1; + c->cache = erealloc(c->cache, c->max*sizeof(Uqid)); + } + nextqid++; + c->cache[c->n] = (Uqid){nextqid, pp, id, t, idx}; + c->n++; + return (nextqid << 8) | t; +} + +static Crumb* +crumb(Gitaux *aux, int n) +{ + if(n < aux->ncrumb) + return &aux->crumb[aux->ncrumb - n - 1]; + return nil; +} + +static void +popcrumb(Gitaux *aux) +{ + Crumb *c; + + if(aux->ncrumb > 1){ + c = crumb(aux, 0); + free(c->name); + unref(c->obj); + aux->ncrumb--; + } +} + +static vlong +branchid(Gitaux *aux, char *path) +{ + int i; + + for(i = 0; branches[i]; i++) + if(strcmp(path, branches[i]) == 0) + goto found; + branches = realloc(branches, sizeof(char *)*(i + 2)); + branches[i] = estrdup(path); + branches[i + 1] = nil; + +found: + if(aux){ + if(aux->refpath) + free(aux->refpath); + aux->refpath = estrdup(branches[i]); + } + return i; +} + +static void +obj2dir(Dir *d, Crumb *c, Object *o, char *name) +{ + d->qid = c->qid; + d->atime = c->mtime; + d->mtime = c->mtime; + d->mode = c->mode; + d->name = estrdup9p(name); + d->uid = estrdup9p(username); + d->gid = estrdup9p(groupname); + d->muid = estrdup9p(username); + if(o->type == GBlob || o->type == GTag){ + d->qid.type = 0; + d->mode &= 0777; + d->length = o->size; + } + +} + +static int +rootgen(int i, Dir *d, void *p) +{ + Crumb *c; + + c = crumb(p, 0); + if (i >= nelem(qroot)) + return -1; + d->mode = 0555 | DMDIR; + d->name = estrdup9p(qroot[i]); + d->qid.vers = 0; + d->qid.type = strcmp(qroot[i], "ctl") == 0 ? 0 : QTDIR; + d->qid.path = qpath(nil, i, i, Qroot); + d->uid = estrdup9p(username); + d->gid = estrdup9p(groupname); + d->muid = estrdup9p(username); + d->mtime = c->mtime; + return 0; +} + +static int +branchgen(int i, Dir *d, void *p) +{ + Gitaux *aux; + Dir *refs; + Crumb *c; + int n; + + aux = p; + c = crumb(aux, 0); + refs = nil; + d->qid.vers = 0; + d->qid.type = QTDIR; + d->qid.path = qpath(c, i, branchid(aux, aux->refpath), Qbranch | Internal); + d->mode = 0555 | DMDIR; + d->uid = estrdup9p(username); + d->gid = estrdup9p(groupname); + d->muid = estrdup9p(username); + d->mtime = c->mtime; + d->atime = c->mtime; + if((n = slurpdir(aux->refpath, &refs)) < 0) + return -1; + if(i < n){ + d->name = estrdup9p(refs[i].name); + free(refs); + return 0; + }else{ + free(refs); + return -1; + } +} + +static int +gtreegen(int i, Dir *d, void *p) +{ + Object *o, *l, *e; + Gitaux *aux; + Crumb *c; + int m; + + aux = p; + c = crumb(aux, 0); + e = c->obj; + if(i >= e->tree->nent) + return -1; + m = e->tree->ent[i].mode; + if(e->tree->ent[i].ismod) + o = emptydir(); + else if((o = readobject(e->tree->ent[i].h)) == nil) + sysfatal("could not read object %H: %r", e->tree->ent[i].h); + if(e->tree->ent[i].islink) + if((l = walklink(aux, o->data, o->size, 0, &m)) != nil) + o = l; + d->qid.vers = 0; + d->qid.type = o->type == GTree ? QTDIR : 0; + d->qid.path = qpath(c, i, o->id, aux->qdir); + d->mode = m; + d->atime = c->mtime; + d->mtime = c->mtime; + d->uid = estrdup9p(username); + d->gid = estrdup9p(groupname); + d->muid = estrdup9p(username); + d->name = estrdup9p(e->tree->ent[i].name); + d->length = o->size; + return 0; +} + +static int +gcommitgen(int i, Dir *d, void *p) +{ + Object *o; + Crumb *c; + + c = crumb(p, 0); + o = c->obj; + d->uid = estrdup9p(username); + d->gid = estrdup9p(groupname); + d->muid = estrdup9p(username); + d->mode = 0444; + d->atime = o->commit->ctime; + d->mtime = o->commit->ctime; + d->qid.type = 0; + d->qid.vers = 0; + + switch(i){ + case 0: + d->mode = DMDIR | gitdirmode; + d->name = estrdup9p("tree"); + d->qid.type = QTDIR; + d->qid.path = qpath(c, i, o->id, Qtree); + break; + case 1: + d->name = estrdup9p("parent"); + d->qid.path = qpath(c, i, o->id, Qparent); + break; + case 2: + d->name = estrdup9p("msg"); + d->qid.path = qpath(c, i, o->id, Qmsg); + break; + case 3: + d->name = estrdup9p("hash"); + d->qid.path = qpath(c, i, o->id, Qhash); + break; + case 4: + d->name = estrdup9p("author"); + d->qid.path = qpath(c, i, o->id, Qauthor); + break; + default: + free(d->uid); + free(d->gid); + free(d->muid); + return -1; + } + return 0; +} + + +static int +objgen(int i, Dir *d, void *p) +{ + Gitaux *aux; + Object *o; + Crumb *c; + char name[64]; + Objlist *ols; + Hash h; + + aux = p; + c = crumb(aux, 0); + if(!aux->ols) + aux->ols = mkols(); + ols = aux->ols; + o = nil; + /* We tried to sent it, but it didn't fit */ + if(aux->olslast && ols->idx == i + 1){ + snprint(name, sizeof(name), "%H", aux->olslast->hash); + obj2dir(d, c, aux->olslast, name); + return 0; + } + while(ols->idx <= i){ + if(olsnext(ols, &h) == -1) + return -1; + if((o = readobject(h)) == nil){ + fprint(2, "corrupt object %H\n", h); + return -1; + } + } + if(o != nil){ + snprint(name, sizeof(name), "%H", o->hash); + obj2dir(d, c, o, name); + unref(aux->olslast); + aux->olslast = ref(o); + return 0; + } + return -1; +} + +static void +objread(Req *r, Gitaux *aux) +{ + Object *o; + + o = crumb(aux, 0)->obj; + switch(o->type){ + case GBlob: + readbuf(r, o->data, o->size); + break; + case GTag: + readbuf(r, o->data, o->size); + break; + case GTree: + dirread9p(r, gtreegen, aux); + break; + case GCommit: + dirread9p(r, gcommitgen, aux); + break; + default: + sysfatal("invalid object type %d", o->type); + } +} + +static void +readcommitparent(Req *r, Object *o) +{ + char *buf, *p, *e; + int i, n; + + /* 40 bytes per hash, 1 per nl, 1 for terminator */ + n = o->commit->nparent * (40 + 1) + 1; + buf = emalloc(n); + p = buf; + e = buf + n; + for (i = 0; i < o->commit->nparent; i++) + p = seprint(p, e, "%H\n", o->commit->parent[i]); + readbuf(r, buf, p - buf); + free(buf); +} + +static void +gitattach(Req *r) +{ + Gitaux *aux; + Dir *d; + + if((d = dirstat(".git")) == nil) + sysfatal("git/fs: %r"); + if(getwd(gitdir, sizeof(gitdir)) == nil) + sysfatal("getwd: %r"); + aux = emalloc(sizeof(Gitaux)); + aux->crumb = emalloc(sizeof(Crumb)); + aux->crumb[0].qid = (Qid){Qroot, 0, QTDIR}; + aux->crumb[0].obj = nil; + aux->crumb[0].mode = DMDIR | 0555; + aux->crumb[0].mtime = d->mtime; + aux->crumb[0].name = estrdup("/"); + aux->ncrumb = 1; + r->ofcall.qid = (Qid){Qroot, 0, QTDIR}; + r->fid->qid = r->ofcall.qid; + r->fid->aux = aux; + respond(r, nil); +} + +static Object* +walklink(Gitaux *aux, char *link, int nlink, int ndotdot, int *mode) +{ + char *p, *e, *path; + Object *o, *n; + int i; + + path = emalloc(nlink + 1); + memcpy(path, link, nlink); + cleanname(path); + + o = crumb(aux, ndotdot)->obj; + assert(o->type == GTree); + for(p = path; *p; p = e){ + n = nil; + e = p + strcspn(p, "/"); + if(*e == '/') + *e++ = '\0'; + /* + * cleanname guarantees these show up at the start of the name, + * which allows trimming them from the end of the trail of crumbs + * instead of needing to keep track of full parentage. + */ + if(strcmp(p, "..") == 0) + n = crumb(aux, ++ndotdot)->obj; + else if(o->type == GTree) + for(i = 0; i < o->tree->nent; i++) + if(strcmp(o->tree->ent[i].name, p) == 0){ + *mode = o->tree->ent[i].mode; + n = readobject(o->tree->ent[i].h); + break; + } + o = n; + if(o == nil) + break; + } + free(path); + for(i = 0; o != nil && i < aux->ncrumb; i++) + if(crumb(aux, i)->obj == o) + return nil; + return o; +} + +static char * +objwalk1(Qid *q, Object *o, Crumb *p, Crumb *c, char *name, vlong qdir, Gitaux *aux) +{ + Object *w, *l; + char *e; + int i, m; + + w = nil; + e = nil; + if(!o) + return Eexist; + switch(o->type){ + case GBlob: + e = Enotdir; + break; + case GTree: + q->type = 0; + for(i = 0; i < o->tree->nent; i++){ + if(strcmp(o->tree->ent[i].name, name) != 0) + continue; + m = o->tree->ent[i].mode; + w = readobject(o->tree->ent[i].h); + if(!w && o->tree->ent[i].ismod) + w = emptydir(); + if(w && o->tree->ent[i].islink) + if((l = walklink(aux, w->data, w->size, 1, &m)) != nil) + w = l; + if(!w) + return Ebadobj; + q->type = (w->type == GTree) ? QTDIR : 0; + q->path = qpath(p, i, w->id, qdir); + c->mode = m; + c->mode |= (w->type == GTree) ? (DMDIR|0755) : 0644; + c->obj = w; + break; + } + if(!w) + e = Eexist; + break; + case GCommit: + q->type = 0; + c->mtime = o->commit->mtime; + c->mode = 0644; + assert(qdir == Qcommit || qdir == Qobject || qdir == Qtree || qdir == Qhead || qdir == Qcommitter); + if(strcmp(name, "msg") == 0) + q->path = qpath(p, 0, o->id, Qmsg); + else if(strcmp(name, "parent") == 0) + q->path = qpath(p, 1, o->id, Qparent); + else if(strcmp(name, "hash") == 0) + q->path = qpath(p, 2, o->id, Qhash); + else if(strcmp(name, "author") == 0) + q->path = qpath(p, 3, o->id, Qauthor); + else if(strcmp(name, "committer") == 0) + q->path = qpath(p, 3, o->id, Qcommitter); + else if(strcmp(name, "tree") == 0){ + q->type = QTDIR; + q->path = qpath(p, 4, o->id, Qtree); + unref(c->obj); + c->mode = DMDIR | gitdirmode; + c->obj = readobject(o->commit->tree); + if(c->obj == nil) + sysfatal("could not read object %H: %r", o->commit->tree); + } + else + e = Eexist; + break; + case GTag: + e = Eimpl; + break; + } + return e; +} + +static Object * +readref(char *pathstr) +{ + char buf[128], path[128], *p, *e; + Hash h; + int n, f; + + snprint(path, sizeof(path), "%s", pathstr); + while(1){ + if((f = open(path, OREAD)) == -1) + return nil; + if((n = readn(f, buf, sizeof(buf) - 1)) == -1) + return nil; + close(f); + buf[n] = 0; + if(strncmp(buf, "ref:", 4) != 0) + break; + + p = buf + 4; + while(isspace(*p)) + p++; + if((e = strchr(p, '\n')) != nil) + *e = 0; + snprint(path, sizeof(path), ".git/%s", p); + } + + if(hparse(&h, buf) == -1) + return nil; + + return readobject(h); +} + +static char* +gitwalk1(Fid *fid, char *name, Qid *q) +{ + char path[128]; + Gitaux *aux; + Crumb *c, *o; + char *e; + Dir *d; + Hash h; + + e = nil; + aux = fid->aux; + + q->vers = 0; + if(strcmp(name, "..") == 0){ + popcrumb(aux); + c = crumb(aux, 0); + *q = c->qid; + fid->qid = *q; + return nil; + } + + aux->crumb = realloc(aux->crumb, (aux->ncrumb + 1) * sizeof(Crumb)); + aux->ncrumb++; + c = crumb(aux, 0); + o = crumb(aux, 1); + memset(c, 0, sizeof(Crumb)); + c->mode = o->mode; + c->mtime = o->mtime; + c->obj = o->obj ? ref(o->obj) : nil; + + switch(QDIR(&fid->qid)){ + case Qroot: + if(strcmp(name, "HEAD") == 0){ + *q = (Qid){Qhead, 0, QTDIR}; + c->mode = DMDIR | 0555; + c->obj = readref(".git/HEAD"); + }else if(strcmp(name, "object") == 0){ + *q = (Qid){Qobject, 0, QTDIR}; + c->mode = DMDIR | 0555; + }else if(strcmp(name, "branch") == 0){ + *q = (Qid){Qbranch, 0, QTDIR}; + aux->refpath = estrdup(".git/refs/"); + c->mode = DMDIR | 0555; + }else if(strcmp(name, "ctl") == 0){ + *q = (Qid){Qctl, 0, 0}; + c->mode = 0644; + }else{ + e = Eexist; + } + break; + case Qbranch: + if(strcmp(aux->refpath, ".git/refs/heads") == 0 && strcmp(name, "HEAD") == 0) + snprint(path, sizeof(path), ".git/HEAD"); + else + snprint(path, sizeof(path), "%s/%s", aux->refpath, name); + q->type = QTDIR; + d = dirstat(path); + if(d && d->qid.type == QTDIR) + q->path = qpath(o, Qbranch, branchid(aux, path), Qbranch); + else if(d && (c->obj = readref(path)) != nil) + q->path = qpath(o, Qbranch, c->obj->id, Qcommit); + else + e = Eexist; + if(d != nil) + c->mode = d->mode & ~0222; + free(d); + break; + case Qobject: + if(c->obj){ + e = objwalk1(q, o->obj, o, c, name, Qobject, aux); + }else{ + if(hparse(&h, name) == -1) + return Ebadobj; + if((c->obj = readobject(h)) == nil) + return Ebadobj; + if(c->obj->type == GBlob || c->obj->type == GTag){ + c->mode = 0644; + q->type = 0; + }else{ + c->mode = DMDIR | 0755; + q->type = QTDIR; + } + q->path = qpath(o, Qobject, c->obj->id, Qobject); + q->vers = 0; + } + break; + case Qhead: + e = objwalk1(q, o->obj, o, c, name, Qhead, aux); + break; + case Qcommit: + e = objwalk1(q, o->obj, o, c, name, Qcommit, aux); + break; + case Qtree: + if(strcmp(name, ".git") == 0) + e = Ename; + else + e = objwalk1(q, o->obj, o, c, name, Qtree, aux); + break; + case Qparent: + case Qmsg: + case Qhash: + case Qauthor: + case Qcommitter: + case Qctl: + return Enodir; + default: + return Egreg; + } + + c->name = estrdup(name); + c->qid = *q; + fid->qid = *q; + return e; +} + +static char* +gitclone(Fid *o, Fid *n) +{ + Gitaux *aux, *oaux; + int i; + + oaux = o->aux; + aux = emalloc(sizeof(Gitaux)); + aux->ncrumb = oaux->ncrumb; + aux->crumb = eamalloc(oaux->ncrumb, sizeof(Crumb)); + for(i = 0; i < aux->ncrumb; i++){ + aux->crumb[i] = oaux->crumb[i]; + aux->crumb[i].name = estrdup(oaux->crumb[i].name); + if(aux->crumb[i].obj) + aux->crumb[i].obj = ref(oaux->crumb[i].obj); + } + if(oaux->refpath) + aux->refpath = strdup(oaux->refpath); + aux->qdir = oaux->qdir; + n->aux = aux; + return nil; +} + +static void +gitdestroyfid(Fid *f) +{ + Gitaux *aux; + int i; + + if((aux = f->aux) == nil) + return; + for(i = 0; i < aux->ncrumb; i++){ + if(aux->crumb[i].obj) + unref(aux->crumb[i].obj); + free(aux->crumb[i].name); + } + olsfree(aux->ols); + free(aux->refpath); + free(aux->crumb); + free(aux); +} + +static char * +readctl(Req *r) +{ + char data[1024], ref[512], *s, *e; + int fd, n; + + if((fd = open(".git/HEAD", OREAD)) == -1) + return Erepo; + /* empty HEAD is invalid */ + if((n = readn(fd, ref, sizeof(ref) - 1)) <= 0) + return Erepo; + close(fd); + + s = ref; + ref[n] = 0; + if(strncmp(s, "ref:", 4) == 0) + s += 4; + while(*s == ' ' || *s == '\t') + s++; + if((e = strchr(s, '\n')) != nil) + *e = 0; + if(strstr(s, "refs/") == s) + s += strlen("refs/"); + + snprint(data, sizeof(data), "branch %s\nrepo %s\n", s, gitdir); + readstr(r, data); + return nil; +} + +static void +gitread(Req *r) +{ + char buf[256], *e; + Gitaux *aux; + Object *o; + Qid *q; + + aux = r->fid->aux; + q = &r->fid->qid; + o = crumb(aux, 0)->obj; + e = nil; + + switch(QDIR(q)){ + case Qroot: + dirread9p(r, rootgen, aux); + break; + case Qbranch: + if(o) + objread(r, aux); + else + dirread9p(r, branchgen, aux); + break; + case Qobject: + if(o) + objread(r, aux); + else + dirread9p(r, objgen, aux); + break; + case Qmsg: + readbuf(r, o->commit->msg, o->commit->nmsg); + break; + case Qparent: + readcommitparent(r, o); + break; + case Qhash: + snprint(buf, sizeof(buf), "%H\n", o->hash); + readstr(r, buf); + break; + case Qauthor: + snprint(buf, sizeof(buf), "%s\n", o->commit->author); + readstr(r, buf); + break; + case Qcommitter: + snprint(buf, sizeof(buf), "%s\n", o->commit->committer); + readstr(r, buf); + break; + case Qctl: + e = readctl(r); + break; + case Qhead: + /* Empty repositories have no HEAD */ + if(o == nil) + r->ofcall.count = 0; + else + objread(r, aux); + break; + case Qcommit: + case Qtree: + objread(r, aux); + break; + default: + e = Egreg; + } + respond(r, e); +} + +static void +gitopen(Req *r) +{ + Gitaux *aux; + Crumb *c; + + aux = r->fid->aux; + c = crumb(aux, 0); + switch(r->ifcall.mode&3){ + default: + respond(r, "botched mode"); + break; + case OWRITE: + respond(r, Eperm); + break; + case OREAD: + case ORDWR: + respond(r, nil); + break; + case OEXEC: + if((c->mode & 0111) == 0) + respond(r, Eperm); + else + respond(r, nil); + break; + } +} + +static void +gitstat(Req *r) +{ + Gitaux *aux; + Crumb *c; + + aux = r->fid->aux; + c = crumb(aux, 0); + r->d.uid = estrdup9p(username); + r->d.gid = estrdup9p(groupname); + r->d.muid = estrdup9p(username); + r->d.qid = r->fid->qid; + r->d.mtime = c->mtime; + r->d.atime = c->mtime; + r->d.mode = c->mode; + if(c->obj) + obj2dir(&r->d, c, c->obj, c->name); + else + r->d.name = estrdup9p(c->name); + respond(r, nil); +} + +Srv gitsrv = { + .attach=gitattach, + .walk1=gitwalk1, + .clone=gitclone, + .open=gitopen, + .read=gitread, + .stat=gitstat, + .destroyfid=gitdestroyfid, +}; + +void +usage(void) +{ + fprint(2, "usage: %s [-d]\n", argv0); + fprint(2, "\t-d: debug\n"); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char repo[512]; + int nelt; + Dir *d; + + gitinit(repo, sizeof(repo), &nelt); + if(chdir(repo) == -1) + sysfatal("chdir: %r"); + + ARGBEGIN{ + case 'd': + chatty9p++; + break; + case 'm': + mntpt = EARGF(usage()); + break; + default: + usage(); + break; + }ARGEND; + if(argc != 0) + usage(); + + if((d = dirstat(".git")) == nil) + sysfatal("dirstat .git: %r"); + username = strdup(d->uid); + groupname = strdup(d->gid); + free(d); + + branches = emalloc(sizeof(char*)); + branches[0] = nil; + postmountsrv(&gitsrv, nil, mntpt, MCREATE); + exits(nil); +} --- /dev/null +++ /sys/src/cmd/git/get.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,515 @@ +#include +#include + +#include "git.h" + +char *fetchbranch; +char *upstream = "origin"; +Hash heads[64]; +int nheads; +int listonly; + +/* + * Checks the rules for a refname at + * git/Documentation/protocol-common.txt + */ +int +okrefname(char *s) +{ + if(strcmp(s, "HEAD") == 0) + return 1; + if(strncmp(s, "refs/", 5) == 0) + return okref(s); + return 0; +} + +int +resolveremote(Hash *h, char *ref) +{ + char buf[128], *s; + int r, f; + + ref = strip(ref); + if((r = hparse(h, ref)) != -1) + return r; + /* Slightly special handling: translate remote refs to local ones. */ + if(strcmp(ref, "HEAD") == 0){ + snprint(buf, sizeof(buf), ".git/HEAD"); + }else if(strstr(ref, "refs/heads") == ref){ + ref += strlen("refs/heads"); + snprint(buf, sizeof(buf), ".git/refs/remotes/%s/%s", upstream, ref); + }else if(strstr(ref, "refs/tags") == ref){ + ref += strlen("refs/tags"); + snprint(buf, sizeof(buf), ".git/refs/tags/%s/%s", upstream, ref); + }else{ + return -1; + } + + r = -1; + s = strip(buf); + if((f = open(s, OREAD)) == -1) + return -1; + if(readn(f, buf, sizeof(buf)) >= 40) + r = hparse(h, buf); + close(f); + + if(r == -1 && strstr(buf, "ref:") == buf) + return resolveremote(h, buf + strlen("ref:")); + return r; +} + +int +rename(char *pack, char *idx, Hash h) +{ + char name[128]; + Dir st; + + nulldir(&st); + st.name = name; + snprint(name, sizeof(name), "%H.pack", h); + if(access(name, AEXIST) == 0) + fprint(2, "warning, pack %s already fetched\n", name); + else if(dirwstat(pack, &st) == -1) + return -1; + snprint(name, sizeof(name), "%H.idx", h); + if(access(name, AEXIST) == 0) + fprint(2, "warning, pack %s already indexed\n", name); + else if(dirwstat(idx, &st) == -1) + return -1; + return 0; +} + +int +checkhash(int fd, vlong sz, Hash *hcomp) +{ + DigestState *st; + Hash hexpect; + char buf[Pktmax]; + vlong n, r; + int nr; + + if(sz < 28){ + werrstr("undersize packfile"); + return -1; + } + + st = nil; + n = 0; + while(n != sz - 20){ + nr = sizeof(buf); + if(sz - n - 20 < sizeof(buf)) + nr = sz - n - 20; + r = readn(fd, buf, nr); + if(r != nr) + return -1; + st = sha1((uchar*)buf, nr, nil, st); + n += r; + } + sha1(nil, 0, hcomp->h, st); + if(readn(fd, hexpect.h, sizeof(hexpect.h)) != sizeof(hexpect.h)) + sysfatal("truncated packfile"); + if(!hasheq(hcomp, &hexpect)){ + werrstr("bad hash: %H != %H", *hcomp, hexpect); + return -1; + } + return 0; +} + +int +mkoutpath(char *path) +{ + char s[128]; + char *p; + int fd; + + snprint(s, sizeof(s), "%s", path); + for(p=strchr(s+1, '/'); p; p=strchr(p+1, '/')){ + *p = 0; + if(access(s, AEXIST) != 0){ + fd = create(s, OREAD, DMDIR | 0775); + if(fd == -1) + return -1; + close(fd); + } + *p = '/'; + } + return 0; +} + +int +prefixed(char *s, char *pfx) +{ + return strncmp(s, pfx, strlen(pfx)) == 0; +} + +int +branchmatch(char *br, char *pat) +{ + char name[128]; + + if(prefixed(pat, "refs/heads")) + snprint(name, sizeof(name), "%s", pat); + else if(prefixed(pat, "heads/")) + snprint(name, sizeof(name), "refs/%s", pat); + else + snprint(name, sizeof(name), "refs/heads/%s", pat); + return strcmp(br, name) == 0; +} + +void +fail(char *pack, char *idx, char *msg, ...) +{ + char buf[ERRMAX]; + va_list ap; + + va_start(ap, msg); + snprint(buf, sizeof(buf), msg, ap); + va_end(ap); + + remove(pack); + remove(idx); + fprint(2, "%s", buf); + exits(buf); +} + +void +enqueueparent(Objq *q, Object *o) +{ + Object *p; + int i; + + if(o->type != GCommit) + return; + for(i = 0; i < o->commit->nparent; i++){ + if((p = readobject(o->commit->parent[i])) == nil) + continue; + qput(q, p, 0); + unref(p); + } +} + +void +fmtcaps(Conn *c, char *caps, int ncaps) +{ + char *p, *e; + + p = caps; + e = caps + ncaps; + *p = 0; + if(c->multiack) + p = seprint(p, e, " multi_ack"); + if(c->sideband64k) + p = seprint(p, e, " side-band-64k"); + else if(c->sideband) + p = seprint(p, e, " side-band"); + assert(p != e); +} + +int +sbread(Conn *c, char *buf, int nbuf, char **pbuf) +{ + int n; + + assert(nbuf >= Pktmax); + if(!c->sideband && !c->sideband64k){ + *pbuf = buf; + return readn(c->rfd, buf, nbuf); + }else{ + *pbuf = buf+1; + while(1){ + n = readpkt(c, buf, nbuf); + if(n <= 0) + return n; + else if(buf[0] == 1 && n > 1) + return n - 1; + else if(buf[0] == 3) + fprint(2, "error: %s\n", buf+1); + else if(buf[0] < 1 || buf[0] > 3) + fprint(2, "unknown sideband(%c:%d) data: %s\n", buf[0], buf[0], buf+1); + + } + } +} + +int +fetchpack(Conn *c) +{ + char spinner[] = {'|', '/', '-', '\\'}; + char buf[Pktmax], caps[512], *sp[3], *ep; + char *packtmp, *idxtmp, **ref, *rp; + Hash h, *have, *want; + int nref, refsz, first, nsent; + int i, j, l, n, spin, req, pfd; + vlong packsz; + Objset hadobj; + Object *o; + Objq haveq; + Qelt e; + + nref = 0; + refsz = 16; + first = 1; + have = eamalloc(refsz, sizeof(have[0])); + want = eamalloc(refsz, sizeof(want[0])); + ref = eamalloc(refsz, sizeof(ref[0])); + while(1){ + n = readpkt(c, buf, sizeof(buf)); + if(n == -1) + return -1; + if(n == 0) + break; + + if(first && n > strlen(buf)){ + parsecaps(buf + strlen(buf) + 1, c); + if(c->symfrom[0] != 0) + print("symref %s %s\n", c->symfrom, c->symto); + } + first = 0; + + if(getfields(buf, sp, nelem(sp), 1, " \t\n\r") < 2) + sysfatal("invalid ref line"); + if(strstr(sp[1], "^{}")) + continue; + if(!okrefname(sp[1])) + sysfatal("remote side sent invalid ref: %s", sp[1]); + if(fetchbranch && !branchmatch(sp[1], fetchbranch)) + continue; + else if(strcmp(sp[1], "HEAD") != 0 + && !prefixed(sp[1], "refs/heads/") + && !prefixed(sp[1], "refs/tags/")) + continue; + if(refsz == nref + 1){ + refsz *= 2; + have = earealloc(have, refsz, sizeof(have[0])); + want = earealloc(want, refsz, sizeof(want[0])); + ref = earealloc(ref, refsz, sizeof(ref[0])); + } + if(hparse(&want[nref], sp[0]) == -1) + sysfatal("invalid hash %s", sp[0]); + if (resolveremote(&have[nref], sp[1]) == -1) + memset(&have[nref], 0, sizeof(have[nref])); + ref[nref] = estrdup(sp[1]); + nref++; + } + if(listonly){ + flushpkt(c); + goto showrefs; + } + + if(writephase(c) == -1) + sysfatal("write: %r"); + req = 0; + fmtcaps(c, caps, sizeof(caps)); + for(i = 0; i < nref; i++){ + if(hasheq(&have[i], &want[i])) + goto skip; + for(j = 0; j < i; j++) + if(hasheq(&want[i], &want[j])) + goto skip; + if((o = readobject(want[i])) != nil){ + unref(o); + continue; + } + if(fmtpkt(c, "want %H%s\n", want[i], caps) == -1) + sysfatal("could not send want for %H", want[i]); + caps[0] = 0; + req = 1; +skip:; + } + flushpkt(c); + + nsent = 0; + qinit(&haveq); + osinit(&hadobj); + /* + * We know we have these objects, and we want to make sure that + * they end up at the front of the queue. Send the 'have lines' + * first, and then enqueue their parents for a second round of + * sends. + */ + for(i = 0; i < nref; i++){ + if(hasheq(&have[i], &Zhash) || oshas(&hadobj, have[i])) + continue; + if((o = readobject(have[i])) == nil) + sysfatal("missing exected object: %H", have[i]); + if(fmtpkt(c, "have %H", o->hash) == -1) + sysfatal("write: %r"); + enqueueparent(&haveq, o); + osadd(&hadobj, o); + unref(o); + nsent++; + } + /* + * The other branches we have probably make sense to send, + * since often we'll be pulling a new branch with objects + * that we already have; it's not entirely clear what we + * want to do here. + */ + for(i = 0; i < nheads; i++){ + if((o = readobject(heads[i])) == nil) + sysfatal("missing exected object: %H", have[i]); + if(fmtpkt(c, "have %H", o->hash) == -1) + sysfatal("write: %r"); + enqueueparent(&haveq, o); + osadd(&hadobj, o); + unref(o); + nsent++; + } + /* + * While we could short circuit this and check if upstream has + * acked our objects, for the first 256 haves, this is simple + * enough. + * + * Also, doing multiple rounds of reference discovery breaks + * when using smart http. + */ + while(req && qpop(&haveq, &e) && nsent < 256){ + if(oshas(&hadobj, e.o->hash)) + continue; + if((o = readobject(e.o->hash)) == nil) + sysfatal("missing object we should have: %H", e.o->hash); + if(fmtpkt(c, "have %H", o->hash) == -1) + sysfatal("write: %r"); + enqueueparent(&haveq, o); + osadd(&hadobj, o); + unref(o); + nsent++; + } + osclear(&hadobj); + qclear(&haveq); + if(!req) + flushpkt(c); + if(fmtpkt(c, "done\n") == -1) + sysfatal("write: %r"); + if(!req) + goto showrefs; + if(readphase(c) == -1) + sysfatal("read: %r"); + + if((packtmp = smprint(".git/objects/pack/fetch.%d.pack", getpid())) == nil) + sysfatal("smprint: %r"); + if((idxtmp = smprint(".git/objects/pack/fetch.%d.idx", getpid())) == nil) + sysfatal("smprint: %r"); + if(mkoutpath(packtmp) == -1) + sysfatal("could not create %s: %r", packtmp); + if((pfd = create(packtmp, ORDWR, 0664)) == -1) + sysfatal("could not create %s: %r", packtmp); + + fprint(2, "fetching... "); + packsz = 0; + if(c->multiack){ + for(i = 0; i < nsent; i++){ + if(readpkt(c, buf, sizeof(buf)) == -1) + sysfatal("read: %r"); + if(strncmp(buf, "NAK\n", 4) == 0) + break; + if(strncmp(buf, "ACK ", 4) == 0){ + if(getfields(buf, sp, nelem(sp), 1, " \t") == 2) + break; + continue; + } + sysfatal("bad response: '%s'", buf); + } + } + if(readpkt(c, buf, sizeof(buf)) == -1) + sysfatal("read: %r"); + if(!c->sideband && !c->sideband64k && !c->multiack){ + /* + * Work around torvalds git bug: we get duplicate have lines + * somtimes, even though the protocol is supposed to start the + * pack file immediately. + * + * Skip ahead until we read 'PACK' off the wire + */ + while(1){ + if(readn(c->rfd, buf, 4) != 4) + sysfatal("fetch packfile: short read"); + if(strncmp(buf, "PACK", 4) == 0) + break; + buf[4] = 0; + l = strtol(buf, &ep, 16); + if(ep != buf + 4) + sysfatal("fetch packfile: junk pktline"); + if(readn(c->rfd, buf, l-4) != l-4) + sysfatal("fetch packfile: short read"); + } + if(write(pfd, "PACK", 4) != 4) + sysfatal("write pack header: %r"); + packsz = 4; + } + spin = 0; + while(1){ + n = sbread(c, buf, sizeof buf, &rp); + if(n == 0) + break; + if(n == -1 || write(pfd, rp, n) != n) + sysfatal("fetch packfile: %r"); + if(interactive && spin++ % 100 == 0) + fprint(2, "\b%c", spinner[spin/100 % nelem(spinner)]); + packsz += n; + } + fprint(2, "\n"); + closeconn(c); + if(seek(pfd, 0, 0) == -1) + fail(packtmp, idxtmp, "packfile seek: %r"); + if(checkhash(pfd, packsz, &h) == -1) + fail(packtmp, idxtmp, "corrupt packfile: %r"); + close(pfd); + if(indexpack(packtmp, idxtmp, h) == -1) + fail(packtmp, idxtmp, "could not index fetched pack: %r"); + if(rename(packtmp, idxtmp, h) == -1) + fail(packtmp, idxtmp, "could not rename indexed pack: %r"); + +showrefs: + for(i = 0; i < nref; i++){ + print("remote %s %H local %H\n", ref[i], want[i], have[i]); + free(ref[i]); + } + free(ref); + free(want); + free(have); + return 0; +} + +void +usage(void) +{ + fprint(2, "usage: %s [-dl] [-b br] [-u upstream] remote\n", argv0); + fprint(2, "\t-b br: only fetch matching branch 'br'\n"); + fprint(2, "remote: fetch from this repository\n"); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char *s; + Conn c; + + ARGBEGIN{ + case 'b': fetchbranch=EARGF(usage()); break; + case 'u': upstream=EARGF(usage()); break; + case 'd': chattygit++; break; + case 'l': listonly++; break; + case 'h': + s = EARGF(usage()); + if(nheads < nelem(heads)) + if(hparse(&heads[nheads], s) == 0) + nheads++; + break; + default: + usage(); + break; + }ARGEND; + + gitinit(nil, 0, nil); + if(argc != 1) + usage(); + + if(gitconnect(&c, argv[0], "upload") == -1) + sysfatal("could not dial %s: %r", argv[0]); + if(fetchpack(&c) == -1) + sysfatal("fetch failed: %r"); + closeconn(&c); + exits(nil); +} --- /dev/null +++ /sys/src/cmd/git/git.h Fri Jun 19 00:00:00 2026 @@ -0,0 +1,352 @@ +#include +#include +#include +#include +#include + +/* 9legacy libc lacks IOUNIT */ +#define IOUNIT 32768U + +typedef struct Conn Conn; +typedef struct Hash Hash; +typedef struct Delta Delta; +typedef struct Cinfo Cinfo; +typedef struct Tinfo Tinfo; +typedef struct Object Object; +typedef struct Objset Objset; +typedef struct Pack Pack; +typedef struct Buf Buf; +typedef struct Dirent Dirent; +typedef struct Idxent Idxent; +typedef struct Objlist Objlist; +typedef struct Dtab Dtab; +typedef struct Dblock Dblock; +typedef struct Objq Objq; +typedef struct Qelt Qelt; +typedef struct Idxent Idxent; + +enum { + Pathmax = 512, + Npackcache = 32, + Hashsz = 20, + Pktmax = 65536, + KiB = 1024, + MiB = 1024*KiB, +}; + +enum { + GNone = 0, + GCommit = 1, + GTree = 2, + GBlob = 3, + GTag = 4, + GOdelta = 6, + GRdelta = 7, +}; + +enum { + Cloaded = 1 << 0, + Cidx = 1 << 1, + Ccache = 1 << 2, + Cexist = 1 << 3, + Cparsed = 1 << 5, + Cthin = 1 << 6, +}; + +enum { + ConnGit, + ConnGit9, + ConnSsh, + ConnHttp, +}; + +struct Objlist { + int idx; + + int fd; + int state; + int stage; + + Dir *top; + int ntop; + int topidx; + Dir *loose; + int nloose; + int looseidx; + Dir *pack; + int npack; + int packidx; + int nent; + int entidx; +}; + +struct Hash { + uchar h[20]; +}; + +struct Conn { + int type; + int rfd; + int wfd; + + /* capabilities */ + char symfrom[256]; + char symto[256]; + char multiack; + char sideband; + char sideband64k; + char report; + + /* only used by http */ + int cfd; + char *url; /* note, first GET uses a different url */ + char *dir; + char *direction; +}; + +struct Dirent { + char *name; + int mode; + Hash h; + char ismod; + char islink; +}; + +struct Object { + /* Git data */ + Hash hash; + int type; + + /* Cache */ + int id; + int flag; + int refs; + Object *next; + Object *prev; + + /* For indexing */ + vlong off; + vlong len; + u32int crc; + + /* Everything below here gets cleared */ + char *all; + char *data; + /* size excludes header */ + vlong size; + + /* Significant win on memory use */ + union { + Cinfo *commit; + Tinfo *tree; + }; +}; + +struct Tinfo { + /* Tree */ + Dirent *ent; + int nent; +}; + +struct Cinfo { + /* Commit */ + Hash *parent; + int nparent; + Hash tree; + char *author; + char *committer; + char *msg; + int nmsg; + vlong ctime; + vlong mtime; +}; + +struct Objset { + Object **obj; + int nobj; + int sz; +}; + +struct Qelt { + Object *o; + vlong ctime; + int color; +}; + +struct Objq { + Qelt *heap; + int nheap; + int heapsz; +}; + +struct Dtab { + Object *o; + uchar *base; + int nbase; + Dblock *b; + int nb; + int sz; +}; + +struct Dblock { + uchar *buf; + int len; + int off; + u64int hash; +}; + +struct Delta { + int cpy; + int off; + int len; +}; + +struct Idxent { + char *path; + Qid qid; + int mode; + int order; + char state; +}; + +#define GETBE16(b)\ + ((((b)[0] & 0xFFul) << 8) | \ + (((b)[1] & 0xFFul) << 0)) + +#define GETBE32(b)\ + ((((b)[0] & 0xFFul) << 24) | \ + (((b)[1] & 0xFFul) << 16) | \ + (((b)[2] & 0xFFul) << 8) | \ + (((b)[3] & 0xFFul) << 0)) +#define GETBE64(b)\ + ((((b)[0] & 0xFFull) << 56) | \ + (((b)[1] & 0xFFull) << 48) | \ + (((b)[2] & 0xFFull) << 40) | \ + (((b)[3] & 0xFFull) << 32) | \ + (((b)[4] & 0xFFull) << 24) | \ + (((b)[5] & 0xFFull) << 16) | \ + (((b)[6] & 0xFFull) << 8) | \ + (((b)[7] & 0xFFull) << 0)) + +#define PUTBE16(b, n)\ + do{ \ + (b)[0] = (n) >> 8; \ + (b)[1] = (n) >> 0; \ + } while(0) + +#define PUTBE32(b, n)\ + do{ \ + (b)[0] = (n) >> 24; \ + (b)[1] = (n) >> 16; \ + (b)[2] = (n) >> 8; \ + (b)[3] = (n) >> 0; \ + } while(0) + +#define PUTBE64(b, n)\ + do{ \ + (b)[0] = (n) >> 56; \ + (b)[1] = (n) >> 48; \ + (b)[2] = (n) >> 40; \ + (b)[3] = (n) >> 32; \ + (b)[4] = (n) >> 24; \ + (b)[5] = (n) >> 16; \ + (b)[6] = (n) >> 8; \ + (b)[7] = (n) >> 0; \ + } while(0) + +#define QDIR(qid) ((int)(qid)->path & (0xff)) +#define isblank(c) \ + (((c) != '\n') && isspace(c)) + +extern Reprog *authorpat; +extern Objset objcache; +extern vlong cachemax; +extern Hash Zhash; +extern int chattygit; +extern int interactive; +extern int gitdirmode; + +#pragma varargck type "H" Hash +#pragma varargck type "T" int +#pragma varargck type "O" Object* +#pragma varargck type "Q" Qid +int Hfmt(Fmt*); +int Tfmt(Fmt*); +int Ofmt(Fmt*); +int Qfmt(Fmt*); + +void gitinit(char*, int, int*); + +/* object io */ +int resolverefs(Hash **, char *); +int resolveref(Hash *, char *); +int listrefs(Hash **, char ***); +Object *ancestor(Object *, Object *); +int findtwixt(Hash *, int, Hash *, int, Object ***, int *); +Object *readobject(Hash); +Object *clearedobject(Hash, int); +int expandprefix(Hash*, Hash, int); +void parseobject(Object *); +int indexpack(char *, char *, Hash); +int writepack(int, Hash*, int, Hash*, int, Hash*); +int hasheq(Hash *, Hash *); +Object *ref(Object *); +void unref(Object *); +void cache(Object *); +Object *emptydir(void); +int entcmp(void*, void*); + +/* object sets */ +void osinit(Objset *); +void osclear(Objset *); +void osadd(Objset *, Object *); +int oshas(Objset *, Hash); +Object *osfind(Objset *, Hash); + +/* object listing */ +Objlist *mkols(void); +int olsnext(Objlist *, Hash *); +void olsfree(Objlist *); + +/* util functions */ +#define dprint(lvl, ...) \ + if(chattygit >= lvl) _dprint(__VA_ARGS__) +void _dprint(char *, ...); +void *eamalloc(ulong, ulong); +void *emalloc(ulong); +void *earealloc(void *, ulong, ulong); +void *erealloc(void *, ulong); +char *estrdup(char *); +int slurpdir(char *, Dir **); +int hparse(Hash *, char *); +int hassuffix(char *, char *); +int swapsuffix(char *, int, char *, char *, char *); +char *strip(char *); +int showprogress(int, int); +u64int murmurhash2(void*, usize); +Qid parseqid(char*); +int charval(int); + +/* packing */ +void dtinit(Dtab *, Object*); +void dtclear(Dtab*); +Delta* deltify(Object*, Dtab*, int*); + +/* proto handling */ +int readpkt(Conn*, char*, int); +int writepkt(Conn*, char*, int); +int fmtpkt(Conn*, char*, ...); +int flushpkt(Conn*); +void initconn(Conn*, int, int); +int gitconnect(Conn *, char *, char *); +int readphase(Conn *); +int writephase(Conn *); +void closeconn(Conn *); +void parsecaps(char *, Conn *); +int okref(char*); + +/* queues */ +void qinit(Objq*); +void qclear(Objq*); +void qput(Objq*, Object*, int); +int qpop(Objq*, Qelt*); --- /dev/null +++ /sys/src/cmd/git/hist Fri Jun 19 00:00:00 2026 @@ -0,0 +1,40 @@ +#!/bin/rc +rfork ne + +. /sys/lib/git/common.rc +gitup + +flagfmt='n:num count'; args='files...' +eval `''{aux/getflags $*} || exec aux/usage +if(~ $#* 0) exec aux/usage + +fn dodiff { + while(t=`{read}){ + h=$t(1) + o=$gitfs/object/`{git/query $h~} + c=$gitfs/object/$h + echo 'Hash:' $h + echo -n 'Date: '; date `{walk -em $c/msg} + echo -n 'Author: '; cat $c/author + echo -n 'Message: '; cat $c/msg + echo + for(f in $files){ + curr=$c/tree/$f + prev=$o/tree/$f + if(! test -e $curr) + curr=/dev/null + if(! test -e $prev) + prev=/dev/null + diff -u $prev $curr + } + echo --' ' + echo ⑨ + echo + } +} + +files=`{cleanname -d $gitrel $*} +args=(-s $files) +if(! ~ $#num 0) + args=(-n $num $args) +git/log $args | dodiff --- /dev/null +++ /sys/src/cmd/git/import Fri Jun 19 00:00:00 2026 @@ -0,0 +1,146 @@ +#!/bin/rc +rfork ne +. /sys/lib/git/common.rc + +diffpath=/tmp/gitimport.$pid.diff +fn sigexit { + rm -f $diffpath +} + +fn apply1 { + adate=`{seconds $adate} + files=`$nl{patch -np1 < $diffpath} + if(! git/walk -q $files){ + >[1=2] { + echo patch would clobber files: + git/walk $files + exit clobber + } + } + echo applying $msg | sed 1q + if(! files=`$nl{patch -p1 < $diffpath}) + die 'patch failed' + for(f in $files){ + if(test -e $f) + git/add $f + if not + git/add -r $f + } + git/walk -fRMA $files + if(~ $#nocommit 0){ + if(hash=`{git/save -n $aname -e $amail -N $name -E $email -m $msg -d $adate $parents $files}){ + echo $hash > $refpath + for(f in $files) + echo T NOQID 0 $f >> .git/INDEX9 + } + } + exit '' +} + + +fn apply @{ + git/fs + amail='' + aname='' + msg='' + whoami + parents='-p'^`{git/query HEAD} + branch=`{git/branch} + if(test -e $gitfs/branch/$branch/tree) + refpath=.git/refs/$branch + if not if(test -e $gitfs/object/$branch/tree) + refpath=.git/HEAD + if not + die 'invalid branch:' $branch + awk ' + function doapply(){ + if(aname == "" || amail == "" || date == "" || gotmsg == "") + exit("missing headers"); + printf "%s", aname > "/env/aname" + printf "%s", amail > "/env/amail" + printf "%s", date > "/env/adate" + if(system("rc -c apply1") != 0) + exit("patch failed"); + close("/env/aname") + close("/env/amail") + close("/env/adate") + close(ENVIRON["diffpath"]) + applied = 1 + } + BEGIN{ + state="headers" + } + state=="diff" && !/^[-+ @]|^diff|^$|^\\ No newline at end of file$/{ + state="footers"; + doapply(); + } + state=="footers" && /^⑨$/{ + state="headers" + next + } + state=="headers" && /^[fF]rom:/ { + sub(/^From:[ \t]*/, "", $0); + aname=$0; + amail=$0; + sub(/[ \t]*<.*$/, "", aname); + sub(/^[^<]*[^>]*$/, "", amail); + } + state=="headers" && /^[Dd]ate:/{ + sub(/^Date:[ \t]*/, "", $0) + date=$0 + } + state=="headers" && /^Subject:/{ + sub(/^Subject:[ \t]*([Rr][Ee]:)?[ \t]*(\[[^\]]*\][ \t]*)*/, "", $0); + gotmsg = 1 + print > "/env/msg" + } + state=="headers" && /^$/ { + state="body" + } + (state=="headers" || state=="body") && (/^diff / || /^---( |$)/){ + state="diff" + next + } + state=="body" && /^[ ]*$/ { + empty=1 + next + } + state=="body" { + if(empty) + printf "\n" > "/env/msg" + empty=0 + sub(/[ ]+$/, "") + print > "/env/msg" + } + state=="diff" { + print > ENVIRON["diffpath"] + } + END{ + if(!failed && state == "diff" && doapply() != 0){ + print "unable to apply patch" > "/fd/2" + exit("mispatch") + } + } + ' +} + +gitup + +flagfmt='n:nocommit'; args='file ...' +eval `''{aux/getflags $*} || exec aux/usage + +patches=(/fd/0) +if(! ~ $#* 0) + patches=`{cleanname -d $gitrel $*} +for(p in $patches){ + # upas serves the decoded header and body separately, + # so we cat them together when applying a upas message. + # + # this allows mime-encoded or line-wrapped patches. + if(test -d $p && test -f $p/header && test -f $p/body) + {{cat $p/header; echo; cat $p/body} | apply} || die $status + if not + apply < $p +} +exit '' --- /dev/null +++ /sys/src/cmd/git/init Fri Jun 19 00:00:00 2026 @@ -0,0 +1,46 @@ +#!/bin/rc -e +rfork ne +. /sys/lib/git/common.rc + +flagfmt='u:upstream upstream,b:branch branch'; args='name' +eval `''{aux/getflags $*} || exec aux/usage + +dir=$1 +if(~ $#dir 0) + dir=. +if(~ $#branch 0) + branch=front +if(test -e $dir/.git) + die $dir/.git already exists +name=`{basename `{cleanname -d `{pwd} $dir}} + +mkdir -p $dir + +cd $dir + +mkdir -p .git/refs/^(heads remotes) +mkdir -p .git/^(fs objects) + +>.git/INDEX9 +>.git/HEAD { + echo ref: refs/heads/$branch +} + +if(~ $#upstream 0){ + upstream=`{git/conf 'defaults "origin".baseurl'} + if(! ~ $#upstream 0) + upstream=$upstream/$name +} + +>.git/config { + echo '[core]' + echo ' repositoryformatversion = p9.0' + if(! ~ $#upstream 0){ + echo '[remote "origin"]' + echo ' url = '$upstream + } + echo '[branch "'$branch'"]' + echo ' remote = origin' +} + +exit '' --- /dev/null +++ /sys/src/cmd/git/log.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,295 @@ +#include +#include +#include "git.h" + +typedef struct Pfilt Pfilt; +struct Pfilt { + char *elt; + int show; + Pfilt *sub; + int nsub; +}; + +Biobuf *out; +char *queryexpr; +char *commitid; +int shortlog; +int msgcount = -1; + +Objset done; +Objq objq; +Pfilt *pathfilt; + +void +filteradd(Pfilt *pf, char *path) +{ + char *p, *e; + int i; + + if((e = strchr(path, '/')) != nil) + p = smprint("%.*s", (int)(e - path), path); + else + p = strdup(path); + + while(e != nil && *e == '/') + e++; + for(i = 0; i < pf->nsub; i++){ + if(strcmp(pf->sub[i].elt, p) == 0){ + pf->sub[i].show = pf->sub[i].show || (e == nil); + if(e != nil) + filteradd(&pf->sub[i], e); + free(p); + return; + } + } + pf->sub = earealloc(pf->sub, pf->nsub+1, sizeof(Pfilt)); + pf->sub[pf->nsub].elt = p; + pf->sub[pf->nsub].show = (e == nil); + pf->sub[pf->nsub].nsub = 0; + pf->sub[pf->nsub].sub = nil; + if(e != nil) + filteradd(&pf->sub[pf->nsub], e); + pf->nsub++; +} + +Hash +lookup(Pfilt *pf, Object *o) +{ + int i; + + if(o == nil) + return Zhash; + for(i = 0; i < o->tree->nent; i++) + if(strcmp(o->tree->ent[i].name, pf->elt) == 0) + return o->tree->ent[i].h; + return Zhash; +} + +int +matchesfilter1(Pfilt *pf, Object *t, Object *pt) +{ + Object *a, *b; + Hash ha, hb; + int i, r; + + if(pf->show) + return 1; + if(t != nil){ + if(pt != nil && t->type != pt->type) + return 1; + if(t->type != GTree) + return 0; + } + + for(i = 0; i < pf->nsub; i++){ + ha = lookup(&pf->sub[i], t); + hb = lookup(&pf->sub[i], pt); + if(hasheq(&ha, &hb)) + continue; + a = readobject(ha); + b = readobject(hb); + r = matchesfilter1(&pf->sub[i], a, b); + unref(a); + unref(b); + if(r) + return 1; + } + return 0; +} + +int +matchesfilter(Object *o) +{ + Object *t, *p, *pt; + int i, r; + + assert(o->type == GCommit); + if(pathfilt == nil) + return 1; + if((t = readobject(o->commit->tree)) == nil) + sysfatal("read %H: %r", o->commit->tree); + for(i = 0; i < o->commit->nparent; i++){ + if((p = readobject(o->commit->parent[i])) == nil) + sysfatal("read %H: %r", o->commit->parent[i]); + if((pt = readobject(p->commit->tree)) == nil) + sysfatal("read %H: %r", o->commit->tree); + r = matchesfilter1(pathfilt, t, pt); + unref(p); + unref(pt); + if(r) + return 1; + } + if(o->commit->nparent == 0) + return matchesfilter1(pathfilt, t, nil); + return 0; +} + + +static char* +nextline(char *p, char *e) +{ + for(; p != e; p++) + if(*p == '\n') + break; + return p; +} + +static int +show(Object *o) +{ + char *p, *q, *e; + + assert(o->type == GCommit); + if(shortlog){ + p = o->commit->msg; + e = p + o->commit->nmsg; + q = nextline(p, e); + Bprint(out, "%H ", o->hash); + Bwrite(out, p, q - p); + Bputc(out, '\n'); + }else{ + Bprint(out, "Hash:\t%H\n", o->hash); + Bprint(out, "Author:\t%s\n", o->commit->author); + if(o->commit->committer != nil + && strcmp(o->commit->author, o->commit->committer) != 0) + Bprint(out, "Committer:\t%s\n", o->commit->committer); + Bprint(out, "Date:\t%s", ctime(o->commit->mtime)); + Bprint(out, "\n"); + p = o->commit->msg; + e = p + o->commit->nmsg; + for(; p != e; p = q){ + q = nextline(p, e); + Bputc(out, '\t'); + Bwrite(out, p, q - p); + Bputc(out, '\n'); + if(q != e) + q++; + } + Bprint(out, "\n"); + } + Bflush(out); + return 1; +} + +static void +showquery(char *q) +{ + Object *o; + Hash *h; + int n, i; + + if((n = resolverefs(&h, q)) == -1) + sysfatal("resolve: %r"); + for(i = 0; i < n && (msgcount == -1 || msgcount > 0); i++){ + if((o = readobject(h[i])) == nil) + sysfatal("read %H: %r", h[i]); + if(matchesfilter(o)){ + show(o); + if(msgcount != -1) + msgcount--; + } + unref(o); + } + exits(nil); +} + +static void +showcommits(char *c) +{ + Object *o, *p; + Qelt e; + int i; + Hash h; + + if(c == nil) + c = "HEAD"; + if(resolveref(&h, c) == -1) + sysfatal("resolve %s: %r", c); + if((o = readobject(h)) == nil) + sysfatal("load %H: %r", h); + if(o->type != GCommit) + sysfatal("%s: not a commit", c); + qinit(&objq); + osinit(&done); + qput(&objq, o, 0); + while(qpop(&objq, &e) && (msgcount == -1 || msgcount > 0)){ + if(matchesfilter(e.o)){ + show(e.o); + if(msgcount != -1) + msgcount--; + } + for(i = 0; i < e.o->commit->nparent; i++){ + if(oshas(&done, e.o->commit->parent[i])) + continue; + if((p = readobject(e.o->commit->parent[i])) == nil) + sysfatal("load %H: %r", o->commit->parent[i]); + osadd(&done, p); + qput(&objq, p, 0); + } + unref(e.o); + } +} + +static void +usage(void) +{ + fprint(2, "usage: %s [-s] [-e expr | -c commit] files..\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char path[1024], repo[1024], *p, *r; + int i, nrel, nrepo; + + ARGBEGIN{ + case 'e': + queryexpr = EARGF(usage()); + break; + case 'c': + commitid = EARGF(usage()); + break; + case 's': + shortlog++; + break; + case 'n': + msgcount = atoi(EARGF(usage())); + break; + default: + usage(); + break; + }ARGEND; + + gitinit(repo, sizeof(repo), &nrel); + nrepo = strlen(repo); + if(argc != 0){ + if(getwd(path, sizeof(path)) == nil) + sysfatal("getwd: %r"); + if(strncmp(path, repo, nrepo) != 0) + sysfatal("path shifted??"); + p = path + nrepo; + pathfilt = emalloc(sizeof(Pfilt)); + for(i = 0; i < argc; i++){ + if(*argv[i] == '/'){ + if(strncmp(argv[i], repo, nrepo) != 0) + continue; + r = smprint("./%s", argv[i]+nrepo); + }else + r = smprint("./%s/%s", p, argv[i]); + cleanname(r); + filteradd(pathfilt, r); + free(r); + } + } + if(chdir(repo) == -1) + sysfatal("chdir: %r"); + + out = Bfdopen(1, OWRITE); + if(queryexpr != nil) + showquery(queryexpr); + else + showcommits(commitid); + Bterm(out); + exits(nil); +} --- /dev/null +++ /sys/src/cmd/git/merge Fri Jun 19 00:00:00 2026 @@ -0,0 +1,46 @@ +#!/bin/rc -e +rfork ne +. /sys/lib/git/common.rc + +fn merge{ + ourbr=$gitfs/object/$1/tree + basebr=$gitfs/object/$2/tree + theirbr=$gitfs/object/$3/tree + + all=`$nl{{git/query -c $1 $2; git/query -c $2 $3} | sed 's/^..//' | sort | uniq} + for(f in $all){ + ours=$ourbr/$f + base=$basebr/$f + theirs=$theirbr/$f + merge1 ./$f $ours $base $theirs + } +} + +gitup + +flagfmt=''; args='theirs' +eval `''{aux/getflags $*} || exec aux/usage + +if(! ~ $#* 1) + exec aux/usage + +theirs=`{git/query $1} +ours=`{git/query HEAD} +base=`{git/query $theirs ^ ' ' ^ $ours ^ '@'} + +if(~ $base $theirs) + die 'nothing to merge, doofus' +if(! git/walk -q) + die 'dirty work tree, refusing to merge' +if(~ $base $ours){ + >[1=2] echo 'fast forwarding...' + echo $theirs > .git/refs/`{git/branch} + git/revert . + exit '' +} +echo $ours >> .git/merge-parents +echo $theirs >> .git/merge-parents + +merge $ours $base $theirs +echo 'merge complete: remember to commit' +exit '' --- /dev/null +++ /sys/src/cmd/git/mkfile Fri Jun 19 00:00:00 2026 @@ -0,0 +1,63 @@ + +#include + +#include "git.h" + +void +osinit(Objset *s) +{ + s->sz = 16; + s->nobj = 0; + s->obj = eamalloc(s->sz, sizeof(Object*)); +} + +void +osclear(Objset *s) +{ + free(s->obj); +} + +void +osadd(Objset *s, Object *o) +{ + u32int probe; + Object **obj; + int i, sz; + + probe = GETBE32(o->hash.h) % s->sz; + while(s->obj[probe]){ + if(hasheq(&s->obj[probe]->hash, &o->hash)){ + s->obj[probe] = o; + return; + } + probe = (probe + 1) % s->sz; + } + assert(s->obj[probe] == nil); + s->obj[probe] = o; + s->nobj++; + if(s->sz < 2*s->nobj){ + sz = s->sz; + obj = s->obj; + + s->sz *= 2; + s->nobj = 0; + s->obj = eamalloc(s->sz, sizeof(Object*)); + for(i = 0; i < sz; i++) + if(obj[i]) + osadd(s, obj[i]); + free(obj); + } +} + +Object* +osfind(Objset *s, Hash h) +{ + u32int probe; + + for(probe = GETBE32(h.h) % s->sz; s->obj[probe]; probe = (probe + 1) % s->sz) + if(hasheq(&s->obj[probe]->hash, &h)) + return s->obj[probe]; + return 0; +} + +int +oshas(Objset *s, Hash h) +{ + return osfind(s, h) != nil; +} --- /dev/null +++ /sys/src/cmd/git/ols.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,170 @@ +#include +#include +#include +#include "git.h" + +enum { + Sinit, + Siter, +}; + +static int +crackidx(char *path, int *np) +{ + int fd; + char buf[4]; + + if((fd = open(path, OREAD)) == -1) + return -1; + if(seek(fd, 8 + 255*4, 0) == -1) + return -1; + if(readn(fd, buf, sizeof(buf)) != sizeof(buf)) + return -1; + *np = GETBE32(buf); + return fd; +} + +int +isloosedir(char *s) +{ + return strlen(s) == 2 && isxdigit(s[0]) && isxdigit(s[1]); +} + +int +endswith(char *n, char *s) +{ + int nn, ns; + + nn = strlen(n); + ns = strlen(s); + return nn > ns && strcmp(n + nn - ns, s) == 0; +} + +int +olsreadpacked(Objlist *ols, Hash *h) +{ + char *p; + int i, j; + + i = ols->packidx; + j = ols->entidx; + + if(ols->state == Siter) + goto step; + for(i = 0; i < ols->npack; i++){ + if(!endswith(ols->pack[i].name, ".idx")) + continue; + if((p = smprint(".git/objects/pack/%s", ols->pack[i].name)) == nil) + sysfatal("smprint: %r"); + ols->fd = crackidx(p, &ols->nent); + free(p); + if(ols->fd == -1) + continue; + j = 0; + while(j < ols->nent){ + if(readn(ols->fd, h->h, sizeof(h->h)) != sizeof(h->h)) + continue; + ols->state = Siter; + ols->packidx = i; + ols->entidx = j; + return 0; +step: + j++; + } + close(ols->fd); + } + ols->state = Sinit; + return -1; +} + + +int +olsreadloose(Objlist *ols, Hash *h) +{ + char buf[64], *p; + int i, j, n; + + i = ols->topidx; + j = ols->looseidx; + if(ols->state == Siter) + goto step; + for(i = 0; i < ols->ntop; i++){ + if(!isloosedir(ols->top[i].name)) + continue; + if((p = smprint(".git/objects/%s", ols->top[i].name)) == nil) + sysfatal("smprint: %r"); + ols->fd = open(p, OREAD); + free(p); + if(ols->fd == -1) + continue; + while((ols->nloose = dirread(ols->fd, &ols->loose)) > 0){ + j = 0; + while(j < ols->nloose){ + n = snprint(buf, sizeof(buf), "%s%s", ols->top[i].name, ols->loose[j].name); + if(n >= sizeof(buf)) + goto step; + if(hparse(h, buf) == -1) + goto step; + ols->state = Siter; + ols->topidx = i; + ols->looseidx = j; + return 0; +step: + j++; + } + free(ols->loose); + ols->loose = nil; + } + close(ols->fd); + ols->fd = -1; + } + ols->state = Sinit; + return -1; +} + +Objlist* +mkols(void) +{ + Objlist *ols; + + ols = emalloc(sizeof(Objlist)); + if((ols->ntop = slurpdir(".git/objects", &ols->top)) == -1) + sysfatal("read top level: %r"); + if((ols->npack = slurpdir(".git/objects/pack", &ols->pack)) == -1) + ols->pack = nil; + ols->fd = -1; + return ols; +} + +void +olsfree(Objlist *ols) +{ + if(ols == nil) + return; + if(ols->fd != -1) + close(ols->fd); + free(ols->top); + free(ols->loose); + free(ols->pack); + free(ols); +} + +int +olsnext(Objlist *ols, Hash *h) +{ + if(ols->stage == 0){ + if(olsreadloose(ols, h) != -1){ + ols->idx++; + return 0; + } + ols->stage++; + } + if(ols->stage == 1){ + if(olsreadpacked(ols, h) != -1){ + ols->idx++; + return 0; + } + ols->stage++; + } + return -1; +} --- /dev/null +++ /sys/src/cmd/git/pack.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,1841 @@ +#include +#include +#include + +#include "git.h" + +typedef struct Buf Buf; +typedef struct Metavec Metavec; +typedef struct Meta Meta; +typedef struct Compout Compout; +typedef struct Packf Packf; + +#define max(x, y) ((x) > (y) ? (x) : (y)) + +struct Metavec { + Meta **meta; + int nmeta; + int metasz; +}; + +struct Meta { + Object *obj; + vlong path; + vlong mtime; + + /* The best delta we picked */ + Meta *head; + Meta *prev; + Delta *delta; + int ndelta; + int nchain; + + /* Only used for delta window */ + Dtab dtab; + + /* Only used for writing offset deltas */ + vlong off; +}; + +struct Compout { + Biobuf *bfd; + DigestState *st; +}; + +struct Buf { + int len; + int sz; + int off; + char *data; +}; + +struct Packf { + char path[128]; + char *idx; + vlong nidx; + + int refs; + Biobuf *pack; + vlong opentm; +}; + +static int readpacked(Biobuf *, Object *, int); +static Object *readidxobject(Biobuf *, Hash, int); + +Objset objcache; +Object *lruhead; +Object *lrutail; +vlong ncache; +vlong cachemax = 128*MiB; +Packf *packf; +int npackf; +int openpacks; + +static void +clear(Object *o) +{ + if(!o) + return; + + assert(o->refs == 0); + assert((o->flag & Ccache) == 0); + assert(o->flag & Cloaded); + switch(o->type){ + case GCommit: + if(!o->commit) + break; + free(o->commit->parent); + free(o->commit->author); + free(o->commit->committer); + free(o->commit); + o->commit = nil; + break; + case GTree: + if(!o->tree) + break; + free(o->tree->ent); + free(o->tree); + o->tree = nil; + break; + default: + break; + } + + free(o->all); + o->all = nil; + o->data = nil; + o->flag &= ~(Cloaded|Cparsed); +} + +void +unref(Object *o) +{ + if(!o) + return; + o->refs--; + if(o->refs == 0) + clear(o); +} + +Object* +ref(Object *o) +{ + o->refs++; + return o; +} + +void +cache(Object *o) +{ + Object *p; + + if(o == lruhead) + return; + if(o == lrutail) + lrutail = lrutail->prev; + if(!(o->flag & Cexist)){ + osadd(&objcache, o); + o->id = objcache.nobj; + o->flag |= Cexist; + } + if(o->prev != nil) + o->prev->next = o->next; + if(o->next != nil) + o->next->prev = o->prev; + if(lrutail == o){ + lrutail = o->prev; + if(lrutail != nil) + lrutail->next = nil; + }else if(lrutail == nil) + lrutail = o; + if(lruhead) + lruhead->prev = o; + o->next = lruhead; + o->prev = nil; + lruhead = o; + + if(!(o->flag & Ccache)){ + o->flag |= Ccache; + ref(o); + ncache += o->size; + } + while(ncache > cachemax && lrutail != lruhead){ + p = lrutail; + lrutail = p->prev; + if(lrutail != nil) + lrutail->next = nil; + p->flag &= ~Ccache; + p->prev = nil; + p->next = nil; + ncache -= p->size; + unref(p); + } +} + +static int +loadpack(Packf *pf, char *name) +{ + char buf[128]; + int i, ifd; + Dir *d; + + memset(pf, 0, sizeof(Packf)); + snprint(buf, sizeof(buf), ".git/objects/pack/%s.idx", name); + snprint(pf->path, sizeof(pf->path), ".git/objects/pack/%s.pack", name); + /* + * if we already have the pack open, just + * steal the loaded info + */ + for(i = 0; i < npackf; i++){ + if(strcmp(pf->path, packf[i].path) == 0){ + pf->pack = packf[i].pack; + pf->idx = packf[i].idx; + pf->nidx = packf[i].nidx; + packf[i].idx = nil; + packf[i].pack = nil; + return 0; + } + } + if((ifd = open(buf, OREAD)) == -1) + return -1; + if((d = dirfstat(ifd)) == nil){ + close(ifd); + return -1; + } + pf->nidx = d->length; + pf->idx = emalloc(pf->nidx); + if(readn(ifd, pf->idx, pf->nidx) != pf->nidx){ + close(ifd); + free(pf->idx); + free(d); + return -1; + } + close(ifd); + free(d); + return 0; +} + +static void +refreshpacks(void) +{ + Packf *pf, *new; + int i, n, l, nnew; + Dir *d; + + if((n = slurpdir(".git/objects/pack", &d)) == -1) + return; + nnew = 0; + new = eamalloc(n, sizeof(Packf)); + for(i = 0; i < n; i++){ + l = strlen(d[i].name); + if(l > 4 && strcmp(d[i].name + l - 4, ".idx") != 0) + continue; + d[i].name[l - 4] = 0; + if(loadpack(&new[nnew], d[i].name) != -1) + nnew++; + } + for(i = 0; i < npackf; i++){ + pf = &packf[i]; + free(pf->idx); + if(pf->pack != nil) + Bterm(pf->pack); + } + free(packf); + packf = new; + npackf = nnew; + free(d); +} + +static Biobuf* +openpack(Packf *pf) +{ + vlong t; + int i, best; + + if(pf->pack != nil){ + pf->refs++; + return pf->pack; + } + /* + * If we've got more packs open + * than we want cached, try to + * free up the oldest ones. + * + * If we can't find a slot, this + * isn't fatal; we can just use + * another fd. + */ + while(openpacks >= Npackcache){ + t = (1ull<<62)-1; + best = -1; + for(i = 0; i < npackf; i++){ + if(&packf[i] != pf + && packf[i].pack != nil + && packf[i].opentm < t + && packf[i].refs == 0){ + t = packf[i].opentm; + best = i; + } + } + if(best == -1){ + fprint(2, "no available pack slots\n"); + break; + } + Bterm(packf[best].pack); + packf[best].pack = nil; + openpacks--; + } + openpacks++; + pf->opentm = nsec(); + pf->refs++; + if((pf->pack = Bopen(pf->path, OREAD)) == nil) + return nil; + return pf->pack; +} + +static void +closepack(Packf *pf) +{ + pf->refs--; +} + +static u32int +crc32(u32int crc, char *b, int nb) +{ + static u32int crctab[256] = { + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, + 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, + 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, + 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, + 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, + 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, + 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, + 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, + 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, + 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, + 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, + 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, + 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, + 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, + 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, + 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, + 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, + 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, + 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, + 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, + 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, + 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, + 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, + 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, + 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, + 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, + 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, + 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, + 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, + 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, + 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d + }; + int i; + + crc ^= 0xFFFFFFFF; + for(i = 0; i < nb; i++) + crc = (crc >> 8) ^ crctab[(crc ^ b[i]) & 0xFF]; + return crc ^ 0xFFFFFFFF; +} + +int +bappend(void *p, void *src, int len) +{ + Buf *b = p; + char *n; + + while(b->len + len >= b->sz){ + b->sz = b->sz*2 + 64; + n = realloc(b->data, b->sz); + if(n == nil) + return -1; + b->data = n; + } + memmove(b->data + b->len, src, len); + b->len += len; + return len; +} + +int +breadc(void *p) +{ + return Bgetc(p); +} + +int +bdecompress(Buf *d, Biobuf *b, vlong *csz) +{ + vlong o; + + o = Boffset(b); + if(inflatezlib(d, bappend, b, breadc) == -1 || d->data == nil){ + free(d->data); + return -1; + } + if (csz) + *csz = Boffset(b) - o; + return d->len; +} + +int +decompress(void **p, Biobuf *b, vlong *csz) +{ + Buf d = {.len=0, .data=nil, .sz=0}; + + if(bdecompress(&d, b, csz) == -1){ + free(d.data); + return -1; + } + *p = d.data; + return d.len; +} + +static vlong +readvint(char *p, char *ep, char **pp) +{ + int s, c; + vlong n; + + s = 0; + n = 0; + do { + if(p == ep) + return -1; + c = *p++; + n |= (vlong)(c & 0x7f) << s; + s += 7; + } while (c & 0x80 && s < 63); + *pp = p; + return n; +} + +static int +applydelta(Object *dst, Object *base, char *d, int nd) +{ + char *r, *b, *ed, *er; + vlong o, l, n, nr, c; + + ed = d + nd; + b = base->data; + n = readvint(d, ed, &d); + if(n == -1 || n >= 1LL << 31 || n != base->size){ + werrstr("mismatched source size"); + return -1; + } + + nr = readvint(d, ed, &d); + if(nr == -1 || nr >= 1LL << 31){ + werrstr("invalid pack: %r"); + return -1; + } + r = emalloc(nr + 64); + n = snprint(r, 64, "%T %lld", base->type, nr) + 1; + dst->all = r; + dst->type = base->type; + dst->data = r + n; + dst->size = nr; + er = dst->data + nr; + r = dst->data; + + while(d != ed){ + c = *d++; + /* copy from base */ + if(c & 0x80){ + o = 0; + l = 0; + /* Offset in base */ + if(d != ed && (c & 0x01)) o |= *(uchar*)d++ << 0; + if(d != ed && (c & 0x02)) o |= *(uchar*)d++ << 8; + if(d != ed && (c & 0x04)) o |= *(uchar*)d++ << 16; + if(d != ed && (c & 0x08)) o |= *(uchar*)d++ << 24; + + /* Length to copy */ + if(d != ed && (c & 0x10)) l |= *(uchar*)d++ << 0; + if(d != ed && (c & 0x20)) l |= *(uchar*)d++ << 8; + if(d != ed && (c & 0x40)) l |= *(uchar*)d++ << 16; + if(l == 0) l = 0x10000; + + if(o < 0 || l < 0 || l > er - r || o + l > base->size){ + werrstr("garbled delta: out of bounds copy"); + return -1; + } + memmove(r, b + o, l); + r += l; + /* inline data */ + }else{ + if(c > ed - d || c > er - r){ + werrstr("garbled delta: write past object"); + return -1; + } + memmove(r, d, c); + d += c; + r += c; + } + } + if(r != er){ + werrstr("truncated delta"); + return -1; + } + + return nr; +} + +static int +readrdelta(Biobuf *f, Object *o, int nd, int flag) +{ + Object *b; + Hash h; + char *d; + int n; + + d = nil; + if(Bread(f, h.h, sizeof(h.h)) != sizeof(h.h)) + goto error; + if(hasheq(&o->hash, &h)) + goto error; + if((n = decompress(&d, f, nil)) == -1) + goto error; + o->len = Boffset(f) - o->off; + if(d == nil || n != nd) + goto error; + if((b = readidxobject(f, h, flag|Cthin)) == nil) + goto error; + if(applydelta(o, b, d, n) == -1) + goto error; + free(d); + return 0; +error: + free(d); + return -1; +} + +static int +readodelta(Biobuf *f, Object *o, vlong nd, vlong p, int flag) +{ + Object b; + char *d; + vlong r; + int c, n; + + d = nil; + if((c = Bgetc(f)) == -1) + return -1; + r = c & 0x7f; + while(c & 0x80 && r < (1ULL<<56)){ + if((c = Bgetc(f)) == -1) + return -1; + r = ((r + 1)<<7) | (c & 0x7f); + } + + if(r > p || r >= (1ULL<<56)){ + werrstr("junk offset -%lld (from %lld)", r, p); + goto error; + } + if((n = decompress(&d, f, nil)) == -1) + goto error; + o->len = Boffset(f) - o->off; + if(d == nil || n != nd) + goto error; + if(Bseek(f, p - r, 0) == -1) + goto error; + memset(&b, 0, sizeof(Object)); + if(readpacked(f, &b, flag) == -1) + goto error; + if(applydelta(o, &b, d, nd) == -1) + goto error; + clear(&b); + free(d); + return 0; +error: + free(d); + return -1; +} + +static int +readpacked(Biobuf *f, Object *o, int flag) +{ + int c, s, n, t; + vlong l, p; + Buf b; + + p = Boffset(f); + c = Bgetc(f); + if(c == -1) + return -1; + l = c & 0xf; + s = 4; + t = (c >> 4) & 0x7; + if(!t){ + werrstr("unknown type for byte %x at %lld", c, p); + return -1; + } + while(c & 0x80){ + if((c = Bgetc(f)) == -1) + return -1; + l |= (vlong)(c & 0x7f) << s; + s += 7; + } + if(l >= (1ULL << 32)){ + werrstr("object too big"); + return -1; + } + switch(t){ + default: + werrstr("invalid object at %lld", Boffset(f)); + return -1; + case GCommit: + case GTree: + case GTag: + case GBlob: + b.sz = 64 + l; + b.data = emalloc(b.sz); + n = snprint(b.data, 64, "%T %lld", t, l) + 1; + b.len = n; + if(bdecompress(&b, f, nil) == -1){ + free(b.data); + return -1; + } + o->len = Boffset(f) - o->off; + o->type = t; + o->all = b.data; + o->data = b.data + n; + o->size = b.len - n; + break; + case GOdelta: + if(readodelta(f, o, l, p, flag) == -1) + return -1; + break; + case GRdelta: + if(readrdelta(f, o, l, flag) == -1) + return -1; + break; + } + o->flag |= Cloaded|flag; + return 0; +} + +static int +readloose(Biobuf *f, Object *o, int flag) +{ + struct { char *tag; int type; } *p, types[] = { + {"blob", GBlob}, + {"tree", GTree}, + {"commit", GCommit}, + {"tag", GTag}, + {nil}, + }; + char *d, *s, *e, *ed; + vlong sz, n; + int l; + + n = decompress(&d, f, nil); + if(n == -1) + return -1; + + s = d; + ed = d + n; + o->type = GNone; + for(p = types; p->tag; p++){ + l = strlen(p->tag); + if(strncmp(s, p->tag, l) == 0){ + s += l; + o->type = p->type; + while(s != ed && !isspace(*s)) + s++; + break; + } + } + if(o->type == GNone){ + free(o->data); + return -1; + } + sz = strtol(s, &e, 0); + if(e == s || *e++ != 0){ + werrstr("malformed object header"); + goto error; + } + if(sz != n - (e - d)){ + werrstr("mismatched sizes"); + goto error; + } + o->size = sz; + o->data = e; + o->all = d; + o->flag |= Cloaded|flag; + return 0; + +error: + free(d); + return -1; +} + +int +hashcmp(uchar *a, uchar *b, uint nbit) +{ + int x, y, i, r; + + i = nbit/8; + r = memcmp(a, b, i); + if(r != 0 || nbit % 8 == 0) + return r; + x = a[i] >> (8 - nbit % 8); + y = b[i] >> (8 - nbit % 8); + return x - y; +} + +vlong +searchindex(char *idx, int nidx, Hash h, int npfx, Hash *hret) +{ + uint lo, hi, hidx; + vlong o, oo, nent; + int i, r; + void *s; + + o = 8; + if(nidx < 8 + 256*4) + return -1; + if(npfx < 8) + return -1; + /* + * Read the fanout table. The fanout table + * contains 256 entries, corresponsding to + * the first byte of the hash. Each entry + * is a 4 byte big endian integer, containing + * the total number of entries with a leading + * byte <= the table index, allowing us to + * rapidly do a binary search on them. + */ + if (h.h[0] == 0){ + lo = 0; + hi = GETBE32(idx + o); + } else { + o += h.h[0]*4 - 4; + lo = GETBE32(idx + o); + hi = GETBE32(idx + o + 4); + } + if(hi == lo) + goto notfound; + nent = GETBE32(idx + 8 + 255*4); + + /* + * Now that we know the range of hashes that the + * entry may exist in, search them + */ + r = -1; + s = nil; + hidx = -1; + o = 8 + 256*4; + while(lo < hi){ + hidx = (hi + lo)/2; + s = idx + o + hidx*sizeof(h.h); + r = hashcmp(h.h, s, npfx); + if(r < 0) + hi = hidx; + else if(r > 0) + lo = hidx + 1; + else + break; + } + if(r != 0) + goto notfound; + if(hret != nil) + memcpy(hret, s, sizeof(Hash)); + + /* + * We found the entry. If it's 32 bits, then we + * can just return the oset, otherwise the 32 + * bit entry contains the oset to the 64 bit + * entry. + */ + oo = 8; /* Header */ + oo += 256*4; /* Fanout table */ + oo += Hashsz*nent; /* Hashes */ + oo += 4*nent; /* Checksums */ + oo += 4*hidx; /* Offset offset */ + if(oo < 0 || oo + 4 > nidx) + goto err; + i = GETBE32(idx + oo); + o = i & 0xffffffffULL; + /* + * Large offsets (i.e. 64-bit) are encoded as an index + * into the next table with the MSB bit set. + */ + if(o & (1ull << 31)){ + o &= 0x7fffffffULL; + oo = 8; /* Header */ + oo += 256*4; /* Fanout table */ + oo += Hashsz*nent; /* Hashes */ + oo += 4*nent; /* Checksums */ + oo += 4*nent; /* 32-bit Offsets */ + oo += 8*o; /* 64-bit Offset offset */ + if(oo < 0 || oo + 8 >= nidx) + goto err; + o = GETBE64(idx + oo); + } + return o; + +err: + werrstr("out of bounds read"); + return -1; +notfound: + werrstr("not present"); + return -1; +} + +/* + * Scans for non-empty word, copying it into buf. + * Strips off word, leading, and trailing space + * from input. + * + * Returns -1 on empty string or error, leaving + * input unmodified. + */ +static int +scanword(char **str, int *nstr, char *buf, int nbuf) +{ + char *p; + int n, r; + + r = -1; + p = *str; + n = *nstr; + while(n && isblank(*p)){ + n--; + p++; + } + + for(; n && *p && !isspace(*p); p++, n--){ + r = 0; + *buf++ = *p; + nbuf--; + if(nbuf == 0) + return -1; + } + while(n && isblank(*p)){ + n--; + p++; + } + *buf = 0; + *str = p; + *nstr = n; + return r; +} + +static void +nextline(char **str, int *nstr) +{ + char *s; + + if((s = strchr(*str, '\n')) != nil){ + *nstr -= s - *str + 1; + *str = s + 1; + } +} + +static int +parseauthor(char **str, int *nstr, char **name, vlong *time) +{ + char buf[512]; + Resub m[4]; + vlong tz; + char *p; + int n, nm; + + if((p = strchr(*str, '\n')) == nil) + sysfatal("malformed author line"); + n = p - *str; + if(n >= sizeof(buf)) + sysfatal("overlong author line"); + memset(m, 0, sizeof(m)); + memcpy(buf, *str, n); + buf[n] = 0; + *str = p; + *nstr -= n; + + if(!regexec(authorpat, buf, m, nelem(m))) + sysfatal("invalid author line %s", buf); + nm = m[1].ep - m[1].sp; + *name = emalloc(nm + 1); + memcpy(*name, m[1].sp, nm); + buf[nm] = 0; + + nm = m[3].ep - m[3].sp; + memcpy(buf, m[3].sp, nm); + buf[nm] = 0; + tz = atoll(buf); + + nm = m[2].ep - m[2].sp; + memcpy(buf, m[2].sp, nm); + buf[nm] = 0; + *time = atoll(buf) + 3600*(tz/100) + 60*(tz%100); + + return 0; +} + +static void +parsecommit(Object *o) +{ + char *p, *t, buf[512]; + int np; + + p = o->data; + np = o->size; + o->commit = emalloc(sizeof(Cinfo)); + while(1){ + if(scanword(&p, &np, buf, sizeof(buf)) == -1) + break; + if(strcmp(buf, "tree") == 0){ + if(scanword(&p, &np, buf, sizeof(buf)) == -1) + sysfatal("invalid commit: tree missing"); + if(hparse(&o->commit->tree, buf) == -1) + sysfatal("invalid commit: garbled tree"); + }else if(strcmp(buf, "parent") == 0){ + if(scanword(&p, &np, buf, sizeof(buf)) == -1) + sysfatal("invalid commit: missing parent"); + o->commit->parent = realloc(o->commit->parent, ++o->commit->nparent * sizeof(Hash)); + if(!o->commit->parent) + sysfatal("unable to malloc: %r"); + if(hparse(&o->commit->parent[o->commit->nparent - 1], buf) == -1) + sysfatal("invalid commit: garbled parent"); + }else if(strcmp(buf, "author") == 0){ + parseauthor(&p, &np, &o->commit->author, &o->commit->mtime); + }else if(strcmp(buf, "committer") == 0){ + parseauthor(&p, &np, &o->commit->committer, &o->commit->ctime); + }else if(strcmp(buf, "gpgsig") == 0){ + /* just drop it */ + if((t = strstr(p, "-----END PGP SIGNATURE-----")) == nil) + if((t = strstr(p, "-----END SSH SIGNATURE-----")) == nil) + if((t = strstr(p, "-----END SIGNED MESSAGE-----")) == nil) + sysfatal("malformed gpg signature"); + np -= t - p; + p = t; + } + nextline(&p, &np); + } + while (np && isspace(*p)) { + p++; + np--; + } + o->commit->msg = p; + o->commit->nmsg = np; +} + +static int +validname(char *s) +{ + if(*s == 0) + return 0; + if(strcmp(s, ".") == 0 || strcmp(s, "..") == 0) + return 0; + for(; *s; s++){ + if((*s&0xff) < 0x20 || *s == 0x7f || *s == '/'){ + werrstr("invalid character in path element: %02x", *(uchar*)s); + return 0; + } + } + return 1; +} + +static void +parsetree(Object *o) +{ + int m, a, entsz, nent; + Dirent *t, *ent; + char *p, *ep; + + p = o->data; + ep = p + o->size; + + nent = 0; + entsz = 16; + ent = eamalloc(entsz, sizeof(Dirent)); + o->tree = emalloc(sizeof(Tinfo)); + while(p != ep){ + if(nent == entsz){ + entsz *= 2; + ent = earealloc(ent, entsz, sizeof(Dirent)); + } + t = &ent[nent++]; + m = strtol(p, &p, 8); + if(*p != ' ') + sysfatal("malformed tree %H: *p=(%d) %c", o->hash, *p, *p); + p++; + /* + * only the stored permissions for the user + * are relevant; git fills group and world + * bits with whatever -- so to serve with + * useful permissions, replicate the mode + * of the git repo dir. + */ + t->mode = gitdirmode; + t->ismod = 0; + t->islink = 0; + if(m & 0777){ + a = (m & 0777)>>6; + t->mode &= ((a<<6)|(a<<3)|a); + } + if(m == 0160000){ /* module */ + t->mode |= DMDIR; + t->ismod = 1; + }else if(m == 0120000){ /* symlink */ + t->mode = 0; + t->islink = 1; + } + if(m & 0040000) /* dir */ + t->mode |= DMDIR; + if(!validname(p)) + sysfatal("invalid entry: %r"); + t->name = p; + p = memchr(p, 0, ep - p); + if(p == nil || *p++ != 0 || ep - p < sizeof(t->h.h)) + sysfatal("malformed tree %H, remaining %d (%s)", o->hash, (int)(ep - p), p); + memcpy(t->h.h, p, sizeof(t->h.h)); + p += sizeof(t->h.h); + } + o->tree->ent = ent; + o->tree->nent = nent; +} + +static void +parsetag(Object *) +{ +} + +void +parseobject(Object *o) +{ + if(o->flag & Cparsed) + return; + switch(o->type){ + case GTree: parsetree(o); break; + case GCommit: parsecommit(o); break; + case GTag: parsetag(o); break; + default: break; + } + o->flag |= Cparsed; +} + +static Object* +readidxobject(Biobuf *idx, Hash h, int flag) +{ + char path[Pathmax], hbuf[41]; + Object *obj, *new; + int i, r, retried; + Biobuf *f; + vlong o; + + if((obj = osfind(&objcache, h)) != nil){ + if(flag & Cidx){ + /* + * If we're indexing, we need to be careful + * to only return objects within this pack, + * so if the objects exist outside the pack, + * we don't index the wrong copy. + */ + if(!(obj->flag & Cidx)) + return nil; + if(obj->flag & Cloaded) + return obj; + o = Boffset(idx); + if(Bseek(idx, obj->off, 0) == -1) + return nil; + if(readpacked(idx, obj, flag) == -1) + return nil; + if(Bseek(idx, o, 0) == -1) + sysfatal("could not restore offset"); + cache(obj); + return obj; + } + if(obj->flag & Cloaded) + return obj; + } + if(flag & Cthin) + flag &= ~Cidx; + if(flag & Cidx) + return nil; + new = nil; + if(obj == nil){ + new = emalloc(sizeof(Object)); + new->id = objcache.nobj + 1; + new->hash = h; + obj = new; + } + + o = -1; + retried = 0; +retry: + for(i = 0; i < npackf; i++){ + o = searchindex(packf[i].idx, packf[i].nidx, h, SHA1dlen*8, nil); + if(o != -1){ + if((f = openpack(&packf[i])) == nil) + goto error; + if((r = Bseek(f, o, 0)) != -1) + r = readpacked(f, obj, flag); + closepack(&packf[i]); + if(r == -1) + goto error; + parseobject(obj); + cache(obj); + return obj; + } + } + + snprint(hbuf, sizeof(hbuf), "%H", h); + snprint(path, sizeof(path), ".git/objects/%c%c/%s", hbuf[0], hbuf[1], hbuf + 2); + if((f = Bopen(path, OREAD)) != nil){ + if(readloose(f, obj, flag) == -1) + goto errorf; + Bterm(f); + parseobject(obj); + cache(obj); + return obj; + } + + if(o == -1){ + if(retried) + goto error; + retried = 1; + refreshpacks(); + goto retry; + } +errorf: + Bterm(f); +error: + free(new); + return nil; +} + +int +expandprefix(Hash *rh, Hash h, int npfx) +{ + int i, fd, ndir; + char buf[128]; + Dir *d; + + refreshpacks(); + if(npfx < 8 || npfx % 4 != 0) + return -1; + for(i = 0; i < npackf; i++) + if(searchindex(packf[i].idx, packf[i].nidx, h, npfx, rh) != -1) + return 0; + sprint(buf, ".git/objects/%x", h.h[0]); + if((fd = open(buf, OREAD)) == -1) + return -1; + ndir = dirreadall(fd, &d); + close(fd); + if(ndir == -1) + return -1; + for(i = 0; i < ndir; i++){ + snprint(buf, sizeof(buf), "%x%s", h.h[0], d[i].name); + if(hparse(rh, buf) == 0 && hashcmp(h.h, rh->h, npfx) == 0){ + free(d); + return 0; + } + + } + free(d); + return -1; +} + +/* + * Loads and returns a cached object. + */ +Object* +readobject(Hash h) +{ + Object *o; + + if((o = readidxobject(nil, h, 0)) == nil) + return nil; + parseobject(o); + ref(o); + return o; +} + +/* + * Creates and returns a cached, cleared object + * that will get loaded some other time. Useful + * for performance if need to mark that a blob + * exists, but we don't care about its contents. + * + * The refcount of the returned object is 0, so + * it doesn't need to be unrefed. + */ +Object* +clearedobject(Hash h, int type) +{ + Object *o; + + if((o = osfind(&objcache, h)) != nil) + return o; + + o = emalloc(sizeof(Object)); + o->hash = h; + o->type = type; + osadd(&objcache, o); + o->id = objcache.nobj; + o->flag |= Cexist; + return o; +} + +int +objcmp(void *pa, void *pb) +{ + Object *a, *b; + + a = *(Object**)pa; + b = *(Object**)pb; + return memcmp(a->hash.h, b->hash.h, sizeof(a->hash.h)); +} + +static int +hwrite(Biobuf *b, void *buf, int len, DigestState **st) +{ + *st = sha1(buf, len, nil, *st); + return Bwrite(b, buf, len); +} + +static u32int +objectcrc(Biobuf *f, Object *o) +{ + char buf[8096]; + int n, r; + + o->crc = 0; + Bseek(f, o->off, 0); + for(n = o->len; n > 0; n -= r){ + r = Bread(f, buf, n > sizeof(buf) ? sizeof(buf) : n); + if(r == -1) + return -1; + if(r == 0) + return 0; + o->crc = crc32(o->crc, buf, r); + } + return 0; +} + +int +indexpack(char *pack, char *idx, Hash ph) +{ + char hdr[4*3], buf[8]; + int nobj, npct, nvalid, nbig; + int i, n, pct; + Object *o, **obj; + DigestState *st; + char *valid; + Biobuf *f; + Hash h; + int c; + + if((f = Bopen(pack, OREAD)) == nil) + return -1; + if(Bread(f, hdr, sizeof(hdr)) != sizeof(hdr)){ + werrstr("short read on header"); + return -1; + } + if(memcmp(hdr, "PACK\0\0\0\2", 8) != 0){ + werrstr("invalid header"); + return -1; + } + + pct = 0; + npct = 0; + nvalid = 0; + nobj = GETBE32(hdr + 8); + obj = eamalloc(nobj, sizeof(Object*)); + valid = eamalloc(nobj, sizeof(char)); + if(interactive) + fprint(2, "indexing %d objects: 0%%", nobj); + while(nvalid != nobj){ + n = 0; + for(i = 0; i < nobj; i++){ + if(valid[i]){ + n++; + continue; + } + pct = showprogress((npct*100)/nobj, pct); + if(obj[i] == nil){ + o = emalloc(sizeof(Object)); + o->off = Boffset(f); + obj[i] = o; + } + o = obj[i]; + /* + * We can seek around when packing delta chains. + * Be extra careful while we don't know where all + * the objects start. + */ + Bseek(f, o->off, 0); + if(readpacked(f, o, Cidx) == -1) + continue; + sha1((uchar*)o->all, o->size + strlen(o->all) + 1, o->hash.h, nil); + valid[i] = 1; + cache(o); + npct++; + n++; + if(objectcrc(f, o) == -1) + return -1; + } + if(n == nvalid) + sysfatal("fix point reached too early: %d/%d: %r", nvalid, nobj); + nvalid = n; + } + if(interactive) + fprint(2, "\b\b\b\b100%%\n"); + Bterm(f); + + st = nil; + qsort(obj, nobj, sizeof(Object*), objcmp); + if((f = Bopen(idx, OWRITE)) == nil) + return -1; + if(hwrite(f, "\xfftOc\x00\x00\x00\x02", 8, &st) != 8) + goto error; + /* fanout table */ + c = 0; + for(i = 0; i < 256; i++){ + while(c < nobj && (obj[c]->hash.h[0] & 0xff) <= i) + c++; + PUTBE32(buf, c); + if(hwrite(f, buf, 4, &st) == -1) + goto error; + } + for(i = 0; i < nobj; i++){ + o = obj[i]; + if(hwrite(f, o->hash.h, sizeof(o->hash.h), &st) == -1) + goto error; + } + + for(i = 0; i < nobj; i++){ + PUTBE32(buf, obj[i]->crc); + if(hwrite(f, buf, 4, &st) == -1) + goto error; + } + + nbig = 0; + for(i = 0; i < nobj; i++){ + if(obj[i]->off < (1ull<<31)) + PUTBE32(buf, obj[i]->off); + else{ + PUTBE32(buf, (1ull << 31) | nbig); + nbig++; + } + if(hwrite(f, buf, 4, &st) == -1) + goto error; + } + for(i = 0; i < nobj; i++){ + if(obj[i]->off >= (1ull<<31)){ + PUTBE64(buf, obj[i]->off); + if(hwrite(f, buf, 8, &st) == -1) + goto error; + } + } + if(hwrite(f, ph.h, sizeof(ph.h), &st) == -1) + goto error; + sha1(nil, 0, h.h, st); + Bwrite(f, h.h, sizeof(h.h)); + + free(obj); + free(valid); + Bterm(f); + return 0; + +error: + free(obj); + free(valid); + Bterm(f); + return -1; +} + +static int +deltaordercmp(void *pa, void *pb) +{ + Meta *a, *b; + vlong cmp; + + a = *(Meta**)pa; + b = *(Meta**)pb; + if(a->obj->type != b->obj->type) + return a->obj->type - b->obj->type; + cmp = (b->path - a->path); + if(cmp != 0) + return (cmp < 0) ? -1 : 1; + cmp = a->mtime - b->mtime; + if(cmp != 0) + return (cmp < 0) ? -1 : 1; + return memcmp(a->obj->hash.h, b->obj->hash.h, sizeof(a->obj->hash.h)); +} + +static int +writeordercmp(void *pa, void *pb) +{ + Meta *a, *b, *ahd, *bhd; + + a = *(Meta**)pa; + b = *(Meta**)pb; + ahd = (a->head == nil) ? a : a->head; + bhd = (b->head == nil) ? b : b->head; + if(ahd->mtime != bhd->mtime) + return bhd->mtime - ahd->mtime; + if(ahd != bhd) + return (uintptr)bhd - (uintptr)ahd; + if(a->nchain != b->nchain) + return a->nchain - b->nchain; + return a->mtime - b->mtime; +} + +static void +addmeta(Metavec *v, Objset *has, Object *o, vlong pathid, vlong mtime) +{ + Meta *m; + + if(oshas(has, o->hash)) + return; + osadd(has, o); + if(v == nil) + return; + m = emalloc(sizeof(Meta)); + m->obj = o; + m->path = pathid; + m->mtime = mtime; + + if(v->nmeta == v->metasz){ + v->metasz = 2*v->metasz; + v->meta = earealloc(v->meta, v->metasz, sizeof(Meta*)); + } + v->meta[v->nmeta++] = m; +} + +static void +freemeta(Meta *m) +{ + free(m->delta); + free(m); +} + +static int +loadtree(Metavec *v, Objset *has, Hash tree, char *dpath, vlong mtime) +{ + Object *t, *o; + Dirent *e; + vlong dh, eh; + int i, k, r; + char *p; + + if(oshas(has, tree)) + return 0; + if((t = readobject(tree)) == nil) + return -1; + if(t->type != GTree){ + fprint(2, "load: %H: not tree\n", t->hash); + unref(t); + return 0; + } + dh = murmurhash2(dpath, strlen(dpath)); + addmeta(v, has, t, dh, mtime); + for(i = 0; i < t->tree->nent; i++){ + e = &t->tree->ent[i]; + if(oshas(has, e->h)) + continue; + if(e->ismod) + continue; + k = (e->mode & DMDIR) ? GTree : GBlob; + o = clearedobject(e->h, k); + if(k == GTree){ + p = smprint("%s/%s", dpath, e->name); + r = loadtree(v, has, e->h, p, mtime); + free(p); + if(r == -1) + return -1; + }else{ + eh = murmurhash2(e->name, strlen(e->name)); + addmeta(v, has, o, dh^eh, mtime); + } + } + unref(t); + return 0; +} + +static int +loadcommit(Metavec *v, Objset *has, Hash h) +{ + Object *c; + int r; + + if(osfind(has, h)) + return 0; + if((c = readobject(h)) == nil) + return -1; + if(c->type != GCommit){ + fprint(2, "load: %H: not commit\n", c->hash); + unref(c); + return 0; + } + addmeta(v, has, c, 0, c->commit->ctime); + r = loadtree(v, has, c->commit->tree, "", c->commit->ctime); + unref(c); + return r; +} + +static int +readmeta(Hash *theirs, int ntheirs, Hash *ours, int nours, Meta ***m) +{ + Object **obj; + Objset has; + int i, nobj; + Metavec v; + + *m = nil; + osinit(&has); + v.nmeta = 0; + v.metasz = 64; + v.meta = eamalloc(v.metasz, sizeof(Meta*)); + if(findtwixt(theirs, ntheirs, ours, nours, &obj, &nobj) == -1) + sysfatal("load twixt: %r"); + + if(nobj == 0) + return 0; + for(i = 0; i < nours; i++) + if(!hasheq(&ours[i], &Zhash)) + if(loadcommit(nil, &has, ours[i]) == -1) + goto out; + for(i = 0; i < nobj; i++) + if(loadcommit(&v, &has, obj[i]->hash) == -1) + goto out; + osclear(&has); + *m = v.meta; + return v.nmeta; +out: + osclear(&has); + free(v.meta); + return -1; +} + +static int +deltasz(Delta *d, int nd) +{ + int i, sz; + sz = 32; + for(i = 0; i < nd; i++) + sz += d[i].cpy ? 7 : d[i].len + 1; + return sz; +} + +static void +pickdeltas(Meta **meta, int nmeta) +{ + Meta *m, *p; + Object *o; + Delta *d; + int i, j, nd, sz, pct, best; + + pct = 0; + dprint(1, "picking deltas\n"); + if(interactive) + fprint(2, "deltifying %d objects: 0%%", nmeta); + qsort(meta, nmeta, sizeof(Meta*), deltaordercmp); + for(i = 0; i < nmeta; i++){ + m = meta[i]; + pct = showprogress((i*100) / nmeta, pct); + m->delta = nil; + m->ndelta = 0; + if(m->obj->type == GCommit || m->obj->type == GTag) + continue; + if((o = readobject(m->obj->hash)) == nil) + sysfatal("readobject %H: %r", m->obj->hash); + dtinit(&m->dtab, o); + if(i >= 11) + dtclear(&meta[i-11]->dtab); + best = o->size; + for(j = max(0, i - 10); j < i; j++){ + p = meta[j]; + /* long chains make unpacking slow */ + if(p->nchain >= 128 || p->obj->type != o->type) + continue; + d = deltify(o, &p->dtab, &nd); + sz = deltasz(d, nd); + if(sz + 32 < best){ + /* + * if we already picked a best delta, + * replace it. + */ + free(m->delta); + best = sz; + m->delta = d; + m->ndelta = nd; + m->nchain = p->nchain + 1; + m->prev = p; + m->head = p->head; + if(m->head == nil) + m->head = p; + }else + free(d); + } + unref(o); + } + for(i = max(0, nmeta - 10); i < nmeta; i++) + dtclear(&meta[i]->dtab); + if(interactive) + fprint(2, "\b\b\b\b100%%\n"); +} + +static int +compread(void *p, void *dst, int n) +{ + Buf *b; + + b = p; + if(n > b->sz - b->off) + n = b->sz - b->off; + memcpy(dst, b->data + b->off, n); + b->off += n; + return n; +} + +static int +compwrite(void *p, void *buf, int n) +{ + return hwrite(((Compout *)p)->bfd, buf, n, &((Compout*)p)->st); +} + +static int +hcompress(Biobuf *bfd, void *buf, int sz, DigestState **st) +{ + int r; + Buf b ={ + .off=0, + .data=buf, + .sz=sz, + }; + Compout o = { + .bfd = bfd, + .st = *st, + }; + + r = deflatezlib(&o, compwrite, &b, compread, 6, 0); + *st = o.st; + return r; +} + +static void +append(char **p, int *len, int *sz, void *seg, int nseg) +{ + if(*len + nseg >= *sz){ + while(*len + nseg >= *sz) + *sz += *sz/2; + *p = erealloc(*p, *sz); + } + memcpy(*p + *len, seg, nseg); + *len += nseg; +} + +static int +encodedelta(Meta *m, Object *o, Object *b, void **pp) +{ + char *p, *bp, buf[16]; + int len, sz, i, j; + vlong n; + Delta *d; + + sz = 128; + len = 0; + p = emalloc(sz); + + /* base object size */ + buf[0] = b->size & 0x7f; + n = b->size >> 7; + for(i = 1; n > 0; i++){ + buf[i - 1] |= 0x80; + buf[i] = n & 0x7f; + n >>= 7; + } + append(&p, &len, &sz, buf, i); + + /* target object size */ + buf[0] = o->size & 0x7f; + n = o->size >> 7; + for(i = 1; n > 0; i++){ + buf[i - 1] |= 0x80; + buf[i] = n & 0x7f; + n >>= 7; + } + append(&p, &len, &sz, buf, i); + for(j = 0; j < m->ndelta; j++){ + d = &m->delta[j]; + if(d->cpy){ + n = d->off; + assert(n < (1ULL<<32)); + bp = buf + 1; + buf[0] = 0x81; + buf[1] = 0x00; + for(i = 0; i < sizeof(buf); i++) { + buf[0] |= 1<>= 8; + if(n == 0) + break; + } + + n = d->len; + assert(n < (1ULL<<24)); + if(n != 0x10000) { + buf[0] |= 0x1<<4; + for(i = 0; i < sizeof(buf)-4 && n > 0; i++){ + buf[0] |= 1<<(i + 4); + *bp++ = n & 0xff; + n >>= 8; + } + } + append(&p, &len, &sz, buf, bp - buf); + }else{ + n = 0; + while(n != d->len){ + buf[0] = (d->len - n < 127) ? d->len - n : 127; + append(&p, &len, &sz, buf, 1); + append(&p, &len, &sz, o->data + d->off + n, buf[0]); + n += buf[0]; + } + } + } + *pp = p; + return len; +} + +static int +packhdr(char *hdr, int ty, int len) +{ + int i; + + hdr[0] = ty << 4; + hdr[0] |= len & 0xf; + len >>= 4; + for(i = 1; len != 0; i++){ + hdr[i-1] |= 0x80; + hdr[i] = len & 0x7f; + len >>= 7; + } + return i; +} + +static int +packoff(char *hdr, vlong off) +{ + int i, j; + char rbuf[8]; + + rbuf[0] = off & 0x7f; + for(i = 1; (off >>= 7) != 0; i++) + rbuf[i] = (--off & 0x7f)|0x80; + + j = 0; + while(i > 0) + hdr[j++] = rbuf[--i]; + return j; +} + +static int +genpack(int fd, Meta **meta, int nmeta, Hash *h, int odelta) +{ + int i, nh, nd, res, pct, ret; + DigestState *st; + Biobuf *bfd; + Meta *m; + Object *o, *po, *b; + char *p, buf[32]; + + st = nil; + ret = -1; + pct = 0; + dprint(1, "generating pack\n"); + if((fd = dup(fd, -1)) == -1) + return -1; + if((bfd = Bfdopen(fd, OWRITE)) == nil) + return -1; + if(hwrite(bfd, "PACK", 4, &st) == -1) + return -1; + PUTBE32(buf, 2); + if(hwrite(bfd, buf, 4, &st) == -1) + return -1; + PUTBE32(buf, nmeta); + if(hwrite(bfd, buf, 4, &st) == -1) + return -1; + qsort(meta, nmeta, sizeof(Meta*), writeordercmp); + if(interactive) + fprint(2, "writing %d objects: 0%%", nmeta); + for(i = 0; i < nmeta; i++){ + pct = showprogress((i*100)/nmeta, pct); + m = meta[i]; + m->off = Boffset(bfd); + if(m->off == -1) + goto error; + if((o = readobject(m->obj->hash)) == nil) + return -1; + if(m->delta == nil){ + nh = packhdr(buf, o->type, o->size); + if(hwrite(bfd, buf, nh, &st) == -1) + goto error; + if(hcompress(bfd, o->data, o->size, &st) == -1) + goto error; + }else{ + if((b = readobject(m->prev->obj->hash)) == nil) + goto error; + nd = encodedelta(m, o, b, &p); + unref(b); + if(odelta && m->prev->off != 0){ + nh = 0; + nh += packhdr(buf, GOdelta, nd); + nh += packoff(buf+nh, m->off - m->prev->off); + if(hwrite(bfd, buf, nh, &st) == -1) + goto error; + }else{ + nh = packhdr(buf, GRdelta, nd); + po = m->prev->obj; + if(hwrite(bfd, buf, nh, &st) == -1) + goto error; + if(hwrite(bfd, po->hash.h, sizeof(po->hash.h), &st) == -1) + goto error; + } + res = hcompress(bfd, p, nd, &st); + free(p); + if(res == -1) + goto error; + } + unref(o); + } + if(interactive) + fprint(2, "\b\b\b\b100%%\n"); + sha1(nil, 0, h->h, st); + if(Bwrite(bfd, h->h, sizeof(h->h)) == -1) + goto error; + ret = 0; +error: + if(Bterm(bfd) == -1) + return -1; + return ret; +} + +int +writepack(int fd, Hash *theirs, int ntheirs, Hash *ours, int nours, Hash *h) +{ + Meta **meta; + int i, r, nmeta; + + if((nmeta = readmeta(theirs, ntheirs, ours, nours, &meta)) == -1) + return -1; + pickdeltas(meta, nmeta); + r = genpack(fd, meta, nmeta, h, 0); + for(i = 0; i < nmeta; i++) + freemeta(meta[i]); + free(meta); + return r; +} --- /dev/null +++ /sys/src/cmd/git/proto.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,566 @@ +#include +#include +#include + +#include "git.h" + +#define Useragent "useragent git/2.24.1" +#define Contenthdr "headers Content-Type: application/x-git-%s-pack-request" +#define Accepthdr "headers Accept: application/x-git-%s-pack-result" + +enum { + Nproto = 16, + Nport = 16, + Nhost = 256, + Npath = 128, + Nbranch = 32, +}; + +char * +matchcap(char *s, char *cap, int full) +{ + if(strncmp(s, cap, strlen(cap)) == 0) + if(!full || strlen(s) == strlen(cap)) + return s + strlen(cap); + return nil; +} + +void +parsecaps(char *caps, Conn *c) +{ + char *p, *n, *s, *t; + + for(p = caps; p != nil; p = n){ + n = strchr(p, ' '); + if(n != nil) + *n++ = 0; + if(matchcap(p, "report-status", 1) != nil) + c->report = 1; + if(matchcap(p, "multi_ack", 1) != nil) + c->multiack = 1; + else if(matchcap(p, "side-band", 1) != nil) + c->sideband = 1; + else if(matchcap(p, "side-band-64k", 1) != nil) + c->sideband64k = 1; + else if((s = matchcap(p, "symref=", 0)) != nil){ + if((t = strchr(s, ':')) == nil) + continue; + *t++ = '\0'; + snprint(c->symfrom, sizeof(c->symfrom), s); + snprint(c->symto, sizeof(c->symto), t); + } + } +} + + +void +tracepkt(int v, char *pfx, char *b, int n) +{ + char *f; + int o, i; + + if(chattygit < v) + return; + o = 0; + f = emalloc(n*4 + 1); + for(i = 0; i < n; i++){ + if(isprint(b[i])){ + f[o++] = b[i]; + continue; + } + f[o++] = '\\'; + switch(b[i]){ + case '\\': f[o++] = '\\'; break; + case '\n': f[o++] = 'n'; break; + case '\r': f[o++] = 'r'; break; + case '\v': f[o++] = 'v'; break; + case '\0': f[o++] = '0'; break; + default: + f[o++] = 'x'; + f[o++] = "0123456789abcdef"[(b[i]>>4)&0xf]; + f[o++] = "0123456789abcdef"[(b[i]>>0)&0xf]; + break; + } + } + f[o] = '\0'; + fprint(2, "%s %04x:\t%s\n", pfx, n, f); + free(f); +} + +int +readpkt(Conn *c, char *buf, int nbuf) +{ + char len[5]; + char *e; + int n; + + if(readn(c->rfd, len, 4) != 4){ + werrstr("pktline: short read from transport"); + return -1; + } + len[4] = 0; + n = strtol(len, &e, 16); + if(n == 0){ + dprint(1, "=r=> 0000\n"); + return 0; + } + if(e != len + 4 || n <= 4) + sysfatal("pktline: bad length '%s'", len); + n -= 4; + if(n >= nbuf) + abort();//sysfatal("pktline: undersize buffer"); + if(readn(c->rfd, buf, n) != n) + return -1; + if(n > 4 && strncmp(buf, "ERR ", 4) == 0){ + if((e = strrchr(buf, '\n')) != nil) + *e = '\0'; + werrstr("%s", buf + 4); + return -1; + } + buf[n] = 0; + tracepkt(1, "=r=>", buf, n); + return n; +} + +int +writepkt(Conn *c, char *buf, int nbuf) +{ + char len[5]; + + + snprint(len, sizeof(len), "%04x", nbuf + 4); + if(write(c->wfd, len, 4) != 4) + return -1; + if(write(c->wfd, buf, nbuf) != nbuf) + return -1; + tracepkt(1, "<=w=", buf, nbuf); + return 0; +} + +int +fmtpkt(Conn *c, char *fmt, ...) +{ + char pkt[Pktmax]; + va_list ap; + int n; + + va_start(ap, fmt); + n = vsnprint(pkt, sizeof(pkt), fmt, ap); + n = writepkt(c, pkt, n); + va_end(ap); + return n; +} + +int +flushpkt(Conn *c) +{ + dprint(1, "<=w= 0000\n"); + return write(c->wfd, "0000", 4); +} + +static void +grab(char *dst, int n, char *p, char *e) +{ + int l; + + l = e - p; + if(l >= n) + sysfatal("overlong component"); + memcpy(dst, p, l); + dst[l] = 0; +} + +static int +parseuri(char *uri, char *proto, char *host, char *port, char *path) +{ + char *s, *p, *q; + int hasport; + + print("uri: \"%s\"\n", uri); + + p = strstr(uri, "://"); + if(p == nil) + snprint(proto, Nproto, "ssh"); + else if(strncmp(uri, "git+", 4) == 0) + grab(proto, Nproto, uri + 4, p); + else + grab(proto, Nproto, uri, p); + *port = 0; + hasport = 1; + if(strcmp(proto, "git") == 0) + snprint(port, Nport, "9418"); + else if(strncmp(proto, "https", 5) == 0) + snprint(port, Nport, "443"); + else if(strncmp(proto, "http", 4) == 0) + snprint(port, Nport, "80"); + else if(strncmp(proto, "hjgit", 5) == 0) + snprint(port, Nport, "17021"); + else if(strncmp(proto, "gits", 5) == 0) + snprint(port, Nport, "9419"); + else + hasport = 0; + s = (p != nil) ? p + 3 : uri; + p = nil; + if(!hasport){ + p = strstr(s, ":"); + if(p != nil) + p++; + } + if(p == nil) + p = strstr(s, "/"); + if(p == nil || strlen(p) == 1){ + werrstr("missing path"); + return -1; + } + + q = memchr(s, ':', p - s); + if(q){ + grab(host, Nhost, s, q); + grab(port, Nport, q + 1, p); + }else{ + grab(host, Nhost, s, p); + } + + snprint(path, Npath, "%s", p); + return 0; +} + +static int +webclone(Conn *c, char *url) +{ + char buf[16]; + int n, conn; + + if((c->cfd = open("/mnt/web/clone", ORDWR)) < 0) + goto err; + if((n = read(c->cfd, buf, sizeof(buf)-1)) == -1) + goto err; + buf[n] = 0; + conn = atoi(buf); + + /* github will behave differently based on useragent */ + if(write(c->cfd, Useragent, sizeof(Useragent)) == -1) + return -1; + dprint(1, "open url %s\n", url); + if(fprint(c->cfd, "url %s", url) == -1) + goto err; + free(c->dir); + c->dir = smprint("/mnt/web/%d", conn); + return 0; +err: + if(c->cfd != -1) + close(c->cfd); + return -1; +} + +static int +webopen(Conn *c, char *file, int mode) +{ + char path[128]; + int fd; + + snprint(path, sizeof(path), "%s/%s", c->dir, file); + if((fd = open(path, mode)) == -1) + return -1; + return fd; +} + +static int +issmarthttp(Conn *c, char *direction) +{ + char buf[Pktmax+1], svc[128]; + int fd, n; + + if((fd = webopen(c, "contenttype", OREAD)) == -1) + return -1; + n = readn(fd, buf, sizeof(buf) - 1); + close(fd); + if(n == -1) + return -1; + buf[n] = '\0'; + snprint(svc, sizeof(svc), "application/x-git-%s-pack-advertisement", direction); + if(strcmp(svc, buf) != 0){ + werrstr("dumb http protocol not supported"); + return -1; + } + + if((n = readpkt(c, buf, sizeof(buf))) == -1) + sysfatal("http read: %r"); + buf[n] = 0; + snprint(svc, sizeof(svc), "# service=git-%s-pack\n", direction); + if(strncmp(svc, buf, n) != 0){ + werrstr("invalid initial packet line"); + return -1; + } + if(readpkt(c, buf, sizeof(buf)) != 0){ + werrstr("protocol garble: expected flushpkt"); + return -1; + } + return 0; +} + +static int +dialhttp(Conn *c, char *host, char *port, char *path, char *direction) +{ + char *geturl, *suff, *hsep, *psep, *isep; + + suff = ""; + hsep = ""; + psep = ""; + isep = ""; + if(port && strlen(port) != 0) + hsep = ":"; + if(path && path[0] != '/') + psep = "/"; + if(path && path[0] && path[strlen(path)-1] != '/') + isep = "/"; + memset(c, 0, sizeof(*c)); + geturl = smprint("https://%s%s%s%s%s%s%sinfo/refs?service=git-%s-pack", + host, hsep, port, psep, path, suff, isep, direction); + c->type = ConnHttp; + c->url = smprint("https://%s%s%s%s%s%s%sgit-%s-pack", + host, hsep, port, psep, path, suff, isep, direction); + c->cfd = webclone(c, geturl); + free(geturl); + if(c->cfd == -1) + return -1; + c->rfd = webopen(c, "body", OREAD); + c->wfd = -1; + if(c->rfd == -1) + return -1; + if(issmarthttp(c, direction) == -1) + return -1; + c->direction = estrdup(direction); + return 0; +} + +static int +dialssh(Conn *c, char *host, char *, char *path, char *direction) +{ + int pid, pfd[2]; + char cmd[64]; + + if(pipe(pfd) == -1) + sysfatal("unable to open pipe: %r"); + pid = fork(); + if(pid == -1) + sysfatal("unable to fork"); + if(pid == 0){ + close(pfd[1]); + dup(pfd[0], 0); + dup(pfd[0], 1); + snprint(cmd, sizeof(cmd), "git-%s-pack", direction); + dprint(1, "exec ssh '%s' '%s' %s\n", host, cmd, path); + execl("/bin/ssh", "ssh", host, cmd, path, nil); + sysfatal("exec: %r"); + } + close(pfd[0]); + c->type = ConnSsh; + c->rfd = pfd[1]; + c->wfd = dup(pfd[1], -1); + return 0; +} + +static int +githandshake(Conn *c, char *host, char *path, char *direction) +{ + char *p, *e, cmd[512]; + + p = cmd; + e = cmd + sizeof(cmd); + p = seprint(p, e - 1, "git-%s-pack %s", direction, path); + if(host != nil) + p = seprint(p + 1, e, "host=%s", host); + if(writepkt(c, cmd, p - cmd + 1) == -1){ + fprint(2, "failed to write message\n"); + closeconn(c); + return -1; + } + return 0; +} + +static int +dialhjgit(Conn *c, char *host, char *port, char *path, char *direction, int auth) +{ + char *ds; + int pid, pfd[2]; + + if((ds = netmkaddr(host, "tcp", port)) == nil) + return -1; + if(pipe(pfd) == -1) + sysfatal("unable to open pipe: %r"); + pid = fork(); + if(pid == -1) + sysfatal("unable to fork"); + if(pid == 0){ + close(pfd[1]); + dup(pfd[0], 0); + dup(pfd[0], 1); + dprint(1, "exec tlsclient -a %s\n", ds); + if(auth) + execl("/bin/tlsclient", "tlsclient", "-a", ds, nil); + else + execl("/bin/tlsclient", "tlsclient", ds, nil); + sysfatal("exec: %r"); + } + close(pfd[0]); + c->type = ConnGit9; + c->rfd = pfd[1]; + c->wfd = dup(pfd[1], -1); + return githandshake(c, host, path, direction); +} + +void +initconn(Conn *c, int rd, int wr) +{ + c->type = ConnGit; + c->rfd = rd; + c->wfd = wr; +} + +static int +dialgit(Conn *c, char *host, char *port, char *path, char *direction) +{ + char *ds; + int fd; + + if((ds = netmkaddr(host, "tcp", port)) == nil) + return -1; + dprint(1, "dial %s git-%s-pack %s\n", ds, direction, path); + fd = dial(ds, nil, nil, nil); + if(fd == -1) + return -1; + c->type = ConnGit; + c->rfd = fd; + c->wfd = dup(fd, -1); + return githandshake(c, host, path, direction); +} + +static int +servelocal(Conn *c, char *path, char *direction) +{ + int pid, pfd[2]; + + if(pipe(pfd) == -1) + sysfatal("unable to open pipe: %r"); + pid = fork(); + if(pid == -1) + sysfatal("unable to fork"); + if(pid == 0){ + close(pfd[1]); + dup(pfd[0], 0); + dup(pfd[0], 1); + execl("/bin/git/serve", "serve", "-w", nil); + sysfatal("exec: %r"); + } + close(pfd[0]); + c->type = ConnGit; + c->rfd = pfd[1]; + c->wfd = dup(pfd[1], -1); + return githandshake(c, nil, path, direction); +} + +static int +localrepo(char *uri, char *path, int npath) +{ + int fd; + snprint(path, npath, "%s/.git/../", uri); + fd = open(path, OREAD); + if(fd < 0) + return -1; + if(fd2path(fd, path, npath) != 0){ + close(fd); + return -1; + } + close(fd); + return 0; +} + +int +gitconnect(Conn *c, char *uri, char *direction) +{ + char proto[Nproto], host[Nhost], port[Nport], path[Npath]; + + memset(c, 0, sizeof(Conn)); + c->rfd = c->wfd = c->cfd = -1; + + if(localrepo(uri, path, sizeof(path)) == 0) + return servelocal(c, path, direction); + + if(parseuri(uri, proto, host, port, path) == -1){ + werrstr("bad uri %s", uri); + return -1; + } + if(strcmp(proto, "ssh") == 0) + return dialssh(c, host, port, path, direction); + else if(strcmp(proto, "git") == 0) + return dialgit(c, host, port, path, direction); + else if(strcmp(proto, "hjgit") == 0) + return dialhjgit(c, host, port, path, direction, 1); + else if(strcmp(proto, "gits") == 0) + return dialhjgit(c, host, port, path, direction, 0); + else if(strcmp(proto, "http") == 0 || strcmp(proto, "https") == 0) + return dialhttp(c, host, port, path, direction); + werrstr("unknown protocol %s", proto); + return -1; +} + +int +writephase(Conn *c) +{ + char hdr[128]; + int n; + + dprint(1, "start write phase\n"); + if(c->type != ConnHttp) + return 0; + + if(c->wfd != -1) + close(c->wfd); + if(c->cfd != -1) + close(c->cfd); + if((c->cfd = webclone(c, c->url)) == -1) + return -1; + n = snprint(hdr, sizeof(hdr), Contenthdr, c->direction); + if(write(c->cfd, hdr, n) == -1) + return -1; + n = snprint(hdr, sizeof(hdr), Accepthdr, c->direction); + if(write(c->cfd, hdr, n) == -1) + return -1; + if((c->wfd = webopen(c, "postbody", OWRITE)) == -1) + return -1; + c->rfd = -1; + return 0; +} + +int +readphase(Conn *c) +{ + dprint(1, "start read phase\n"); + if(c->type != ConnHttp) + return 0; + if(close(c->wfd) == -1) + return -1; + if((c->rfd = webopen(c, "body", OREAD)) == -1) + return -1; + c->wfd = -1; + return 0; +} + +void +closeconn(Conn *c) +{ + close(c->rfd); + close(c->wfd); + switch(c->type){ + case ConnGit: + break; + case ConnGit9: + case ConnSsh: + free(wait()); + break; + case ConnHttp: + close(c->cfd); + break; + } +} --- /dev/null +++ /sys/src/cmd/git/pull Fri Jun 19 00:00:00 2026 @@ -0,0 +1,88 @@ +#!/bin/rc -e +rfork en +. /sys/lib/git/common.rc + +fn update{ + upstream=$1 + url=$2 + dir=$3 + dflag=() + if(! ~ $#debug 0) + dflag='-d' + heads=`$nl{ + # safe to split in awk, branch names may not + # have spaces or newlines. + git/query `$nl{ + cd .git/refs && \ + walk -f -emp heads remotes | \ + sort -rn | \ + awk '{print $2}' + } + } + if(! ~ $#heads 0) + heads='-h'^$heads + {git/get $dflag -u $upstream $heads $url >[2=3] || die $status} | awk ' + /^remote refs\/(heads|tags)\//{ + ref=$2 + hash=$3 + gsub("^refs/heads", "refs/remotes/'$upstream'", ref) + outfile = ".git/"ref + outdir = outfile + sub("/[^/]*$", "", outdir) + system("mkdir -p " outdir) + print hash > outfile; + close(outfile); + } + ' |[3] tr '\x0d' '\x0a' +} + +gitup + +flagfmt='d:debug, q:quiet, f:fetchonly, + u:upstream upstream' +args='' +eval `''{aux/getflags $*} || exec aux/usage + +if(~ $#upstream 0) + upstream=origin +remote=`$nl{git/conf 'remote "'$upstream'".url'} +if(~ $#remote 0){ + remote=$upstream + upstream=THEM +} + +update $upstream $remote +if (~ $fetchonly 1) + exit + +local=`{git/branch} +remote=`{git/branch | subst '^(refs/)?heads' 'remotes/'$upstream} + +# we have local commits, but the remote hasn't changed. +# in this case, we want to keep the local commits untouched. +if(~ `{git/query HEAD $remote @} `{git/query $remote}){ + echo 'up to date' >[1=2] + exit +} +# The remote repository and our HEAD have diverged: we +# need to merge. +if(! ~ `{git/query HEAD $remote @} `{git/query HEAD}){ + >[1=2]{ + echo ours: `{git/query HEAD} + echo theirs: `{git/query $remote} + echo common: `{git/query HEAD $remote @} + echo git/merge $remote + } + exit diverged +} + +oldcommit=`{git/query $local} +newcommit=`{git/query $remote} + +if(~ $#quiet 0){ + git/log -s -e $oldcommit'..'$newcommit + echo $remote':' $oldcommit '=>' $newcommit +} + +git/branch -mnb $remote $local +exit '' --- /dev/null +++ /sys/src/cmd/git/push Fri Jun 19 00:00:00 2026 @@ -0,0 +1,51 @@ +#!/bin/rc -e +rfork en +. /sys/lib/git/common.rc + +gitup + +flagfmt='a:pushall, b:branch branch, f:force, d:debug, + r:remove remove, u:upstream upstream' args='' +eval `''{aux/getflags $*} || exec aux/usage +if(! ~ $#* 0) + exec aux/usage + +if(~ $pushall 1) + branch=`$nl{cd .git/refs/heads && walk -f} +if(~ $#branch 0) + branch=`{git/branch} +if(~ $#branch 0) + die 'no branches' +if(~ $force 1) + force=-f +if(~ $debug 1) + debug='-d' + +if(~ $#upstream 0) + upstream=origin + +remotes=`$nl{git/conf -a 'remote "'$upstream'".url'} +if(~ $#remotes 0) + remotes=$upstream +branch=-b^$branch +if(! ~ $#remove 0) + remove=-r^$remove +for(remote in $remotes){ + updates=`$nl{git/send $debug $force $branch $remove $remote} || die $status + for(ln in $updates){ + u=`{echo $ln} + refpath=`{echo $u(2) | subst '^refs/heads/' '.git/refs/remotes/'$upstream'/'} + switch($u(1)){ + case update; + mkdir -p `{basename -d $refpath} + echo $u(4) > $refpath + echo $u(2)^':' $u(3) '=>' $u(4) + case delete; + echo $u(2)^': removed' + rm -f $refpath + case uptodate; + echo $u(2)^': up to date' + } + } +} +exit '' --- /dev/null +++ /sys/src/cmd/git/query.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,197 @@ +#include +#include + +#include "git.h" + +#pragma varargck type "P" void + +int fullpath; +int changes; +int reverse; +char *path[128]; +int npath; + +int +Pfmt(Fmt *f) +{ + int i, n; + + n = 0; + for(i = 0; i < npath; i++) + n += fmtprint(f, "%s/", path[i]); + return n; +} + +void +showdir(Hash dh, char *dname, char m) +{ + Dirent *p, *e; + Object *d; + + + path[npath++] = dname; + if((d = readobject(dh)) == nil) + sysfatal("bad hash %H", dh); + assert(d->type == GTree); + p = d->tree->ent; + e = p + d->tree->nent; + for(; p != e; p++){ + if(p->ismod) + continue; + if(p->mode & DMDIR) + showdir(p->h, p->name, m); + else + print("%c %P%s\n", m, p->name); + } + print("%c %P\n", m); + unref(d); + npath--; +} + +void +show(Dirent *e, char m) +{ + if(e->mode & DMDIR) + showdir(e->h, e->name, m); + else + print("%c %P%s\n", m, e->name); +} + +void +difftrees(Object *a, Object *b) +{ + Dirent *ap, *bp, *ae, *be; + int c; + + ap = ae = nil; + bp = be = nil; + if(a != nil){ + if(a->type != GTree) + return; + ap = a->tree->ent; + ae = ap + a->tree->nent; + } + if(b != nil){ + if(b->type != GTree) + return; + bp = b->tree->ent; + be = bp + b->tree->nent; + } + while(ap != ae && bp != be){ + c = entcmp(ap, bp); + if(c == 0){ + if(ap->mode == bp->mode && hasheq(&ap->h, &bp->h)) + goto next; + if(ap->mode != bp->mode) + print("! %P%s\n", ap->name); + else if(!(ap->mode & DMDIR) || !(bp->mode & DMDIR)) + print("@ %P%s\n", ap->name); + if((ap->mode & DMDIR) && (bp->mode & DMDIR)){ + if(npath >= nelem(path)) + sysfatal("path too deep"); + path[npath++] = ap->name; + if((a = readobject(ap->h)) == nil && !ap->ismod) + sysfatal("bad hash %H", ap->h); + if((b = readobject(bp->h)) == nil && !bp->ismod) + sysfatal("bad hash %H", bp->h); + difftrees(a, b); + unref(a); + unref(b); + npath--; + } +next: + ap++; + bp++; + }else if(c < 0) { + show(ap, '-'); + ap++; + }else if(c > 0){ + show(bp, '+'); + bp++; + } + } + for(; ap != ae; ap++) + show(ap, '-'); + for(; bp != be; bp++) + show(bp, '+'); +} + +void +diffcommits(Hash ah, Hash bh) +{ + Object *a, *b, *at, *bt; + + at = nil; + bt = nil; + if(!hasheq(&ah, &Zhash) && (a = readobject(ah)) != nil){ + if(a->type != GCommit) + sysfatal("not commit: %H", ah); + if((at = readobject(a->commit->tree)) == nil) + sysfatal("bad hash %H", a->commit->tree); + unref(a); + } + if(!hasheq(&bh, &Zhash) && (b = readobject(bh)) != nil){ + if(b->type != GCommit) + sysfatal("not commit: %H", ah); + if((bt = readobject(b->commit->tree)) == nil) + sysfatal("bad hash %H", b->commit->tree); + unref(b); + } + difftrees(at, bt); + unref(at); + unref(bt); +} + +void +usage(void) +{ + fprint(2, "usage: %s [-pcr] query\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char *query, repo[512]; + char *p, *e, *objpfx; + int i, j, n, nrel; + Hash *h; + + ARGBEGIN{ + case 'd': chattygit++; break; + case 'p': fullpath++; break; + case 'c': changes++; break; + case 'r': reverse ^= 1; break; + default: usage(); break; + }ARGEND; + + fmtinstall('P', Pfmt); + if(argc == 0) + usage(); + gitinit(repo, sizeof(repo), &nrel); + if(chdir(repo) == -1) + sysfatal("chdir: %r"); + if((objpfx = smprint("%s/.git/fs/object/", repo)) == nil) + sysfatal("smprint: %r"); + for(i = 0, n = 0; i < argc; i++) + n += strlen(argv[i]) + 1; + query = emalloc(n+1); + p = query; + e = query + n; + for(i = 0; i < argc; i++) + p = seprint(p, e, "%s ", argv[i]); + n = resolverefs(&h, query); + free(query); + if(n == -1) + sysfatal("resolve: %r"); + if(changes){ + for(i = 1; i < n; i++) + diffcommits(h[0], h[i]); + }else{ + p = (fullpath ? objpfx : ""); + for(j = 0; j < n; j++) + print("%s%H\n", p, h[reverse ? n - 1 - j : j]); + } + exits(nil); +} + --- /dev/null +++ /sys/src/cmd/git/rebase Fri Jun 19 00:00:00 2026 @@ -0,0 +1,41 @@ +#!/bin/rc -e + +rfork en + +. /sys/lib/git/common.rc +gitup + +flagfmt='s:src src, n:nocommit'; args='onto' +eval `''{aux/getflags $*} || exec aux/usage +if(~ $#* 0) exec aux/usage + +if(~ $#src 0) + src=`{git/branch} +nflag='' +if(~ $nocommit 1) + nflag='-n' + +dst=`{git/query $1} +com=`{git/query $dst $src @} +if(~ $dst $com) + die 'nothing to rebase, doofus' +git/log -se $dst'..'$src | awk ' +BEGIN{ + src=ENVIRON["src"]; + dst=ENVIRON["dst"]; +} +{ + if(!done) + print "git/branch -nb "dst" rebase.wip"; + c=$1; $1=""; + print "git/export "c" | git/import '$nflag'#"$0; + done=1 +} +END{ + if(!done) + print "git/branch -nb "dst" "; + else{ + print "git/branch -nb rebase.wip "src + print "git/branch -r rebase.wip"; + } +}' --- /dev/null +++ /sys/src/cmd/git/ref.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,568 @@ +#include +#include +#include + +#include "git.h" + +typedef struct Eval Eval; + +enum { + Blank, + Keep, + Drop, + Skip, +}; + +enum { + Lca, + Twixt, + Range, +}; + +struct Eval { + char *str; + char *p; + Object **stk; + int nstk; + int stksz; +}; + +static char *colors[] = { +[Keep] "keep", +[Drop] "drop", +[Blank] "blank", +[Skip] "skip", +}; + +static Object zcommit = { + .type=GCommit +}; + +void +eatspace(Eval *ev) +{ + while(isspace(ev->p[0])) + ev->p++; +} + +void +push(Eval *ev, Object *o) +{ + if(ev->nstk == ev->stksz){ + ev->stksz = 2*ev->stksz + 1; + ev->stk = erealloc(ev->stk, ev->stksz*sizeof(Object*)); + } + ev->stk[ev->nstk++] = o; +} + +Object* +pop(Eval *ev) +{ + if(ev->nstk == 0) + sysfatal("stack underflow"); + return ev->stk[--ev->nstk]; +} + +Object* +peek(Eval *ev) +{ + if(ev->nstk == 0) + sysfatal("stack underflow"); + return ev->stk[ev->nstk - 1]; +} + +int +isword(char e) +{ + return isalnum(e) || e == '/' || e == '-' || e == '_' || e == '.'; +} + +int +word(Eval *ev, char *b, int nb) +{ + char *p, *e; + int n; + + p = ev->p; + for(e = p; isword(*e) && strncmp(e, "..", 2) != 0; e++) + /* nothing */; + /* 1 for nul terminator */ + n = e - p + 1; + if(n >= nb) + n = nb; + snprint(b, n, "%s", p); + ev->p = e; + return n > 0; +} + +int +take(Eval *ev, char *m) +{ + int l; + + l = strlen(m); + if(strncmp(ev->p, m, l) != 0) + return 0; + ev->p += l; + return 1; +} + +static int +paint(Hash *head, int nhead, Hash *tail, int ntail, Object ***res, int *nres, int mode) +{ + Qelt e; + Objq objq; + Objset keep, drop, skip; + Object *o, *c, **range; + int i, nskip, nrange; + + osinit(&keep); + osinit(&drop); + osinit(&skip); + qinit(&objq); + range = nil; + nrange = 0; + nskip = 0; + + for(i = 0; i < nhead; i++){ + if(hasheq(&head[i], &Zhash)) + continue; + if((o = readobject(head[i])) == nil){ + fprint(2, "warning: %H does not point at commit\n", head[i]); + werrstr("read head %H: %r", head[i]); + return -1; + } + if(o->type != GCommit){ + fprint(2, "warning: %H does not point at commit\n", o->hash); + unref(o); + continue; + } + dprint(1, "init: keep %H\n", o->hash); + qput(&objq, o, Keep); + unref(o); + } + for(i = 0; i < ntail; i++){ + if(hasheq(&tail[i], &Zhash)) + continue; + if((o = readobject(tail[i])) == nil){ + werrstr("read tail %H: %r", tail[i]); + return -1; + } + if(o->type != GCommit){ + fprint(2, "warning: %H does not point at commit\n", o->hash); + unref(o); + continue; + } + dprint(1, "init: drop %H\n", o->hash); + qput(&objq, o, Drop); + unref(o); + } + + dprint(1, "finding twixt commits\n"); + while(objq.nheap != 0 && qpop(&objq, &e)){ + if(e.color == Skip) + nskip--; + if(oshas(&skip, e.o->hash)) + continue; + switch(e.color){ + case Keep: + if(oshas(&keep, e.o->hash)) + continue; + if(oshas(&drop, e.o->hash)) + e.color = Skip; + else if(mode == Range){ + range = earealloc(range, nrange+1, sizeof(Object*)); + range[nrange++] = e.o; + } + osadd(&keep, e.o); + break; + case Drop: + if(oshas(&drop, e.o->hash)) + continue; + if(oshas(&keep, e.o->hash)) + e.color = Skip; + osadd(&drop, e.o); + break; + case Skip: + osadd(&skip, e.o); + break; + } + o = readobject(e.o->hash); + if(o->type != GCommit){ + werrstr("not a commit: %H", o->hash); + goto error; + } + for(i = 0; i < o->commit->nparent; i++){ + if((c = readobject(e.o->commit->parent[i])) == nil) + goto error; + if(c->type != GCommit){ + fprint(2, "warning: %H does not point at commit\n", c->hash); + unref(c); + continue; + } + dprint(2, "\tenqueue: %s %H\n", colors[e.color], c->hash); + qput(&objq, c, e.color); + unref(c); + if(e.color == Skip) + nskip++; + } + unref(o); + } + switch(mode){ + case Lca: + dprint(1, "found ancestor\n"); + o = nil; + for(i = 0; i < keep.sz; i++){ + o = keep.obj[i]; + if(o != nil && oshas(&drop, o->hash) && !oshas(&skip, o->hash)) + break; + } + if(i == keep.sz){ + *nres = 0; + *res = nil; + }else{ + *nres = 1; + *res = eamalloc(1, sizeof(Object*)); + (*res)[0] = o; + } + break; + case Twixt: + dprint(1, "found twixt\n"); + *res = eamalloc(keep.nobj, sizeof(Object*)); + *nres = 0; + for(i = 0; i < keep.sz; i++){ + o = keep.obj[i]; + if(o != nil && !oshas(&drop, o->hash) && !oshas(&skip, o->hash)){ + (*res)[*nres] = o; + (*nres)++; + } + } + break; + case Range: + dprint(1, "found range\n"); + *res = eamalloc(nrange, sizeof(Object*)); + *nres = 0; + for(i = nrange - 1; i >= 0; i--){ + o = range[i]; + if(!oshas(&drop, o->hash) && !oshas(&skip, o->hash)){ + (*res)[*nres] = o; + (*nres)++; + } + } + free(range); + break; + } + osclear(&keep); + osclear(&drop); + osclear(&skip); + return 0; +error: + dprint(1, "paint error: %r\n"); + free(objq.heap); + free(range); + return -1; +} + +int +findtwixt(Hash *head, int nhead, Hash *tail, int ntail, Object ***res, int *nres) +{ + return paint(head, nhead, tail, ntail, res, nres, Twixt); +} + +Object* +ancestor(Object *a, Object *b) +{ + Object **o, *r; + int n; + + if(paint(&a->hash, 1, &b->hash, 1, &o, &n, Lca) == -1 || n == 0) + return nil; + r = ref(o[0]); + free(o); + return r; +} + +int +lca(Eval *ev) +{ + Object *a, *b, **o; + int n; + + if(ev->nstk < 2){ + werrstr("ancestor needs 2 objects"); + return -1; + } + n = 0; + b = pop(ev); + a = pop(ev); + paint(&a->hash, 1, &b->hash, 1, &o, &n, Lca); + if(n == 0) + return -1; + push(ev, *o); + free(o); + return 0; +} + +static int +parent(Eval *ev) +{ + Object *o, *p; + + o = pop(ev); + if(o->type != GCommit){ + werrstr("not a commit: %H", o->hash); + return -1; + } + /* Special case: first commit has no parent. */ + if(o->commit->nparent == 0) + p = emptydir(); + else if ((p = readobject(o->commit->parent[0])) == nil){ + werrstr("no parent for %H", o->hash); + return -1; + } + + push(ev, p); + return 0; +} + +static int +range(Eval *ev) +{ + Object *a, *b, **o; + int i, n; + + b = pop(ev); + a = pop(ev); + if(hasheq(&b->hash, &Zhash)) + b = &zcommit; + if(hasheq(&a->hash, &Zhash)) + a = &zcommit; + if(a->type != GCommit || b->type != GCommit){ + werrstr("non-commit object in range"); + return -1; + } + + if(paint(&b->hash, 1, &a->hash, 1, &o, &n, Range) == -1) + return -1; + for(i = 0; i < n; i++) + push(ev, o[i]); + free(o); + return 0; +} + +static int +matchpfx(Hash *h, char *ref) +{ + int i, c; + Hash pfx; + char *p; + + memset(&pfx, 0, sizeof(Hash)); + for(i = 0, p = ref; *p; p++, i++){ + if((c = charval(*p)) == -1) + return -1; + pfx.h[i/2] |= c; + if((i & 1) == 0) + pfx.h[i/2] <<= 4; + } + return expandprefix(h, pfx, i*4); +} + +int +readref(Hash *h, char *ref) +{ + static char *try[] = {"", "refs/", "refs/heads/", "refs/remotes/", "refs/tags/", nil}; + char buf[256], s[256], **pfx; + int r, f, n; + + if((r = hparse(h, ref)) != -1) + return r; + if(strcmp(ref, "HEAD") == 0){ + snprint(buf, sizeof(buf), ".git/HEAD"); + if((f = open(buf, OREAD)) == -1) + return -1; + if((n = readn(f, s, sizeof(s) - 1))== -1) + return -1; + s[n] = 0; + strip(s); + r = hparse(h, s); + goto found; + } + for(pfx = try; *pfx; pfx++){ + snprint(buf, sizeof(buf), ".git/%s%s", *pfx, ref); + if((f = open(buf, OREAD)) == -1) + continue; + if((n = readn(f, s, sizeof(s) - 1)) == -1) + continue; + s[n] = 0; + strip(s); + r = hparse(h, s); + close(f); + goto found; + } + if((r = matchpfx(h, ref)) != -1) + return r; + return -1; + +found: + if(r == -1 && strncmp(s, "ref: ", 5) == 0) + r = readref(h, s + 5); + return r; +} + +int +evalpostfix(Eval *ev) +{ + char name[256]; + Object *o; + Hash h; + + eatspace(ev); + if(!word(ev, name, sizeof(name))){ + werrstr("expected name in expression"); + return -1; + } + if(readref(&h, name) == -1){ + werrstr("invalid ref %s", name); + return -1; + } + if(hasheq(&h, &Zhash)) + o = &zcommit; + else if((o = readobject(h)) == nil){ + werrstr("invalid ref %s (hash %H)", name, h); + return -1; + } + push(ev, o); + + while(1){ + eatspace(ev); + switch(ev->p[0]){ + case '^': + case '~': + ev->p++; + if(parent(ev) == -1) + return -1; + break; + case '@': + ev->p++; + if(lca(ev) == -1) + return -1; + break; + default: + goto done; + break; + } + } +done: + return 0; +} + +int +evalexpr(Eval *ev, char *ref) +{ + memset(ev, 0, sizeof(*ev)); + ev->str = ref; + ev->p = ref; + + while(1){ + if(evalpostfix(ev) == -1) + return -1; + if(ev->p[0] == '\0') + return 0; + else if(take(ev, ":") || take(ev, "..")){ + if(evalpostfix(ev) == -1) + return -1; + if(ev->p[0] != '\0'){ + werrstr("junk at end of expression"); + return -1; + } + return range(ev); + } + } +} + +int +resolverefs(Hash **r, char *ref) +{ + Eval ev; + Hash *h; + int i; + + if(evalexpr(&ev, ref) == -1){ + free(ev.stk); + return -1; + } + h = eamalloc(ev.nstk, sizeof(Hash)); + for(i = 0; i < ev.nstk; i++) + h[i] = ev.stk[i]->hash; + *r = h; + free(ev.stk); + return ev.nstk; +} + +int +resolveref(Hash *r, char *ref) +{ + Eval ev; + + if(evalexpr(&ev, ref) == -1){ + free(ev.stk); + return -1; + } + if(ev.nstk != 1){ + werrstr("ambiguous ref expr"); + free(ev.stk); + return -1; + } + *r = ev.stk[0]->hash; + free(ev.stk); + return 0; +} + +int +readrefdir(Hash **refs, char ***names, int *nrefs, char *dpath, char *dname) +{ + Dir *d, *e, *dir; + char *path, *name, *sep; + int ndir; + + if((ndir = slurpdir(dpath, &dir)) == -1) + return -1; + sep = (*dname == '\0') ? "" : "/"; + e = dir + ndir; + for(d = dir; d != e; d++){ + path = smprint("%s/%s", dpath, d->name); + name = smprint("%s%s%s", dname, sep, d->name); + if(d->mode & DMDIR) { + if(readrefdir(refs, names, nrefs, path, name) == -1) + goto noref; + }else{ + *refs = erealloc(*refs, (*nrefs + 1)*sizeof(Hash)); + *names = erealloc(*names, (*nrefs + 1)*sizeof(char*)); + if(resolveref(&(*refs)[*nrefs], name) == -1) + goto noref; + (*names)[*nrefs] = name; + *nrefs += 1; + goto next; + } +noref: free(name); +next: free(path); + } + free(dir); + return 0; +} + +int +listrefs(Hash **refs, char ***names) +{ + int nrefs; + + *refs = nil; + *names = nil; + nrefs = 0; + if(readrefdir(refs, names, &nrefs, ".git/refs", "") == -1){ + free(*refs); + return -1; + } + return nrefs; +} --- /dev/null +++ /sys/src/cmd/git/repack.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,85 @@ +#include +#include + +#include "git.h" + +#define TMPPATH(suff) (".git/objects/pack/repack."suff) + +int +cleanup(Hash h) +{ + char newpfx[42], dpath[256], fpath[256]; + int i, j, nd; + Dir *d; + + snprint(newpfx, sizeof(newpfx), "%H.", h); + for(i = 0; i < 256; i++){ + snprint(dpath, sizeof(dpath), ".git/objects/%02x", i); + if((nd = slurpdir(dpath, &d)) == -1) + continue; + for(j = 0; j < nd; j++){ + snprint(fpath, sizeof(fpath), ".git/objects/%02x/%s", i, d[j].name); + remove(fpath); + } + remove(dpath); + free(d); + } + snprint(dpath, sizeof(dpath), ".git/objects/pack"); + if((nd = slurpdir(dpath, &d)) == -1) + return -1; + for(i = 0; i < nd; i++){ + if(strncmp(d[i].name, newpfx, strlen(newpfx)) == 0) + continue; + snprint(fpath, sizeof(fpath), ".git/objects/pack/%s", d[i].name); + remove(fpath); + } + return 0; +} + +void +usage(void) +{ + fprint(2, "usage: %s [-d]\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char path[128], **names; + int fd, nrefs; + Hash *refs, h; + Dir rn; + + ARGBEGIN{ + case 'd': + chattygit++; + break; + default: + usage(); + }ARGEND; + + gitinit(nil, 0, nil); + refs = nil; + if((nrefs = listrefs(&refs, &names)) == -1) + sysfatal("load refs: %r"); + if((fd = create(TMPPATH("pack.tmp"), OWRITE, 0644)) == -1) + sysfatal("open %s: %r", TMPPATH("pack.tmp")); + if(writepack(fd, refs, nrefs, nil, 0, &h) == -1) + sysfatal("writepack: %r"); + if(indexpack(TMPPATH("pack.tmp"), TMPPATH("idx.tmp"), h) == -1) + sysfatal("indexpack: %r"); + close(fd); + + nulldir(&rn); + rn.name = path; + snprint(path, sizeof(path), "%H.pack", h); + if(dirwstat(TMPPATH("pack.tmp"), &rn) == -1) + sysfatal("rename pack: %r"); + snprint(path, sizeof(path), "%H.idx", h); + if(dirwstat(TMPPATH("idx.tmp"), &rn) == -1) + sysfatal("rename pack: %r"); + if(cleanup(h) == -1) + sysfatal("cleanup: %r"); + exits(nil); +} --- /dev/null +++ /sys/src/cmd/git/revert Fri Jun 19 00:00:00 2026 @@ -0,0 +1,22 @@ +#!/bin/rc +rfork en +. /sys/lib/git/common.rc + +gitup + +flagfmt='c:query query' args='file ...' +if (! eval `''{aux/getflags $*} || ~ $#* 0) + exec aux/usage + +if(~ $#query 0) + query=HEAD +commit=`{git/query -p $query} + +files=`$nl{cleanname -d $gitrel $* | drop $gitroot} +for(f in `$nl{git/walk -c -fRM -b $query $files}){ + mkdir -p `{basename -d $f} + cp -x -- $commit/tree/$f $f + touch -c $f + git/add $f +} +exit '' --- /dev/null +++ /sys/src/cmd/git/rm Fri Jun 19 00:00:00 2026 @@ -0,0 +1,3 @@ +#!/bin/rc -e + +exec git/add -r $* --- /dev/null +++ /sys/src/cmd/git/save.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,485 @@ +#include +#include +#include "git.h" + +typedef struct Objbuf Objbuf; +struct Objbuf { + int off; + char *hdr; + int nhdr; + char *dat; + int ndat; +}; + +enum { + Maxparents = 16, +}; + +char *authorname; +char *authoremail; +char *committername; +char *committeremail; +char *commitmsg; +Hash parents[Maxparents]; +int nparents; +Idxent *idx; +int idxsz; +int nidx; + +int +gitmode(Dirent *e) +{ + if(e->islink) + return 0120000; + else if(e->ismod) + return 0160000; + else if(e->mode & DMDIR) + return 0040000; + else if(e->mode & 0100) + return 0100755; + else + return 0100644; +} + +int +namecmp(void *pa, void *pb) +{ + return strcmp(*(char**)pa, *(char**)pb); +} + +int +idxcmp(void *pa, void *pb) +{ + Idxent *a, *b; + int c; + + a = (Idxent*)pa; + b = (Idxent*)pb; + if((c = strcmp(a->path, b->path)) != 0) + return c; + assert(a->order != b->order); + return a-> order < b->order ? -1 : 1; +} + +static int +bwrite(void *p, void *buf, int nbuf) +{ + return Bwrite(p, buf, nbuf); +} + +static int +objbytes(void *p, void *buf, int nbuf) +{ + Objbuf *b; + int r, n, o; + char *s; + + b = p; + n = 0; + if(b->off < b->nhdr){ + r = b->nhdr - b->off; + r = (nbuf < r) ? nbuf : r; + memcpy(buf, b->hdr, r); + b->off += r; + nbuf -= r; + n += r; + } + if(b->off < b->ndat + b->nhdr){ + s = buf; + o = b->off - b->nhdr; + r = b->ndat - o; + r = (nbuf < r) ? nbuf : r; + memcpy(s + n, b->dat + o, r); + b->off += r; + n += r; + } + return n; +} + +void +writeobj(Hash *h, char *hdr, int nhdr, char *dat, int ndat) +{ + Objbuf b = {.off=0, .hdr=hdr, .nhdr=nhdr, .dat=dat, .ndat=ndat}; + char s[64], o[256]; + SHA1state *st; + Biobuf *f; + int fd; + + st = sha1((uchar*)hdr, nhdr, nil, nil); + st = sha1((uchar*)dat, ndat, nil, st); + sha1(nil, 0, h->h, st); + + snprint(s, sizeof(s), "%H", *h); + fd = create(".git/objects", OREAD, DMDIR|0755); + close(fd); + snprint(o, sizeof(o), ".git/objects/%c%c", s[0], s[1]); + fd = create(o, OREAD, DMDIR | 0755); + close(fd); + snprint(o, sizeof(o), ".git/objects/%c%c/%s", s[0], s[1], s + 2); + if(readobject(*h) == nil){ + if((f = Bopen(o, OWRITE)) == nil) + sysfatal("could not open %s: %r", o); + if(deflatezlib(f, bwrite, &b, objbytes, 9, 0) == -1) + sysfatal("could not write %s: %r", o); + Bterm(f); + } +} + +int +writetree(Dirent *ent, int nent, Hash *h) +{ + char *t, *txt, *etxt, hdr[128]; + int nhdr, n; + Dirent *d, *p; + + t = emalloc((16+256+20) * nent); + txt = t; + etxt = t + (16+256+20) * nent; + + /* sqeeze out deleted entries */ + n = 0; + p = ent; + for(d = ent; d != ent + nent; d++) + if(d->name) + p[n++] = *d; + nent = n; + + qsort(ent, nent, sizeof(Dirent), entcmp); + for(d = ent; d != ent + nent; d++){ + if(strlen(d->name) >= 255) + sysfatal("overly long filename: %s", d->name); + t = seprint(t, etxt, "%o %s", gitmode(d), d->name) + 1; + memcpy(t, d->h.h, sizeof(d->h.h)); + t += sizeof(d->h.h); + } + nhdr = snprint(hdr, sizeof(hdr), "%T %lld", GTree, (vlong)(t - txt)) + 1; + writeobj(h, hdr, nhdr, txt, t - txt); + free(txt); + return nent; +} + +void +blobify(Dir *d, char *path, int *mode, Hash *bh) +{ + char h[64], *buf; + int f, nh; + + if((d->mode & DMDIR) != 0) + sysfatal("not file: %s", path); + *mode = d->mode; + nh = snprint(h, sizeof(h), "%T %lld", GBlob, d->length) + 1; + if((f = open(path, OREAD)) == -1) + sysfatal("could not open %s: %r", path); + buf = emalloc(d->length); + if(readn(f, buf, d->length) != d->length) + sysfatal("could not read blob %s: %r", path); + writeobj(bh, h, nh, buf, d->length); + free(buf); + close(f); +} + +int +tracked(char *path) +{ + int r, lo, hi, mid; + + lo = 0; + hi = nidx-1; + while(lo <= hi){ + mid = (hi + lo) / 2; + r = strcmp(path, idx[mid].path); + if(r < 0) + hi = mid-1; + else if(r > 0) + lo = mid+1; + else + return idx[mid].state != 'R'; + } + return 0; +} + +Dirent* +dirent(Dirent **ent, int *nent, char *name) +{ + Dirent *d; + + assert(strchr(name, '/') == nil); + + for(d = *ent; d != *ent + *nent; d++) + if(d->name && strcmp(d->name, name) == 0) + return d; + *nent += 1; + *ent = erealloc(*ent, *nent * sizeof(Dirent)); + d = *ent + (*nent - 1); + memset(d, 0, sizeof(*d)); + d->name = estrdup(name); + return d; +} + +int +treeify(Object *t, char **path, char **epath, int off, Hash *h) +{ + int nent, ne, slash, isdir; + char *s, **p, **ep; + Dirent *e, *ent; + Object *o; + Dir *d; + + nent = t->tree->nent; + ent = eamalloc(nent, sizeof(*ent)); + memcpy(ent, t->tree->ent, nent*sizeof(*ent)); + for(p = path; p != epath; p = ep){ + s = *p; + + /* + * paths have been normalized already, + * no leading or double-slashes allowed. + */ + assert(off <= strlen(s)); + assert(off == 0 || s[off-1] == '/'); + assert(s[off] != '\0' && s[off] != '/'); + + /* get next path element length (from off until '/' or nul) */ + for(ne = 1; s[off+ne] != '\0' && s[off+ne] != '/'; ne++) + ; + + /* truncate at '/' or nul */ + slash = s[off + ne]; + s[off + ne] = '\0'; + + /* skip over children (having s as prefix) */ + for(ep = p + 1; slash && ep != epath; ep++){ + if(strncmp(s, *ep, off + ne) != 0) + break; + if((*ep)[off+ne] != '\0' && (*ep)[off+ne] != '/') + break; + } + + d = dirstat(s); + e = dirent(&ent, &nent, s + off); + if(e->islink) + sysfatal("symlinks may not be modified: %s", s); + if(e->ismod) + sysfatal("submodules may not be modified: %s", s); + + s[off + ne] = slash; + + isdir = d != nil && (d->mode & DMDIR) != 0; + /* + * exist? slash? dir? track? + * n _ _ _ -> remove: file gone + * y n n y -> blob: tracked non-dir + * y n y n -> remove: file untracked + * y n y n -> remove: file -> dir + * y n y y -> remove: file -> dir + * y n y n -> untracked dir, cli junk + * y y y n -> recurse + * y y y y -> recurse + */ + if(d == nil || !slash && isdir && tracked(s)){ + /* + * if a tracked file is removed or turned + * into a dir, we want to delete it. We + * only want to change files passed in, and + * not ones along the way, so ignore files + * that have a '/'. + */ + e->name = nil; + s[off + ne] = slash; + continue; + } else if(slash && isdir){ + /* + * If we have a list of entries that go into + * a directory, create a tree node for this + * entry, and recurse down. + */ + e->mode = DMDIR | 0755; + o = readobject(e->h); + if(o == nil || o->type != GTree) + o = emptydir(); + /* + * if after processing deletions, a tree is empty, + * mark it for removal from the parent. + * + * Note, it is still written to the object store, + * but this is fine -- and ensures that an empty + * repository will continue to work. + */ + if(treeify(o, p, ep, off + ne + 1, &e->h) == 0) + e->name = nil; + }else if(!slash && !isdir){ + /* + * If the file was explicitly passed in and is + * not a dir, we want to either remove it or + * track it, depending on the state of the index. + */ + if(tracked(s) && !isdir) + blobify(d, s, &e->mode, &e->h); + else + e->name = nil; + } + free(d); + } + if(nent == 0) + sysfatal("%.*s: refusing to update empty directory", off, *path); + nent = writetree(ent, nent, h); + free(ent); + return nent; +} + + +void +mkcommit(Hash *c, vlong date, Hash tree) +{ + char *s, h[64]; + int ns, nh, i; + Fmt f; + + fmtstrinit(&f); + fmtprint(&f, "tree %H\n", tree); + for(i = 0; i < nparents; i++) + fmtprint(&f, "parent %H\n", parents[i]); + fmtprint(&f, "author %s <%s> %lld +0000\n", authorname, authoremail, date); + fmtprint(&f, "committer %s <%s> %lld +0000\n", committername, committeremail, date); + fmtprint(&f, "\n"); + fmtprint(&f, "%s", commitmsg); + s = fmtstrflush(&f); + + ns = strlen(s); + nh = snprint(h, sizeof(h), "%T %d", GCommit, ns) + 1; + writeobj(c, h, nh, s, ns); + free(s); +} + +Object* +findroot(void) +{ + Object *t, *c; + Hash h; + + if(resolveref(&h, "HEAD") == -1) + return emptydir(); + if((c = readobject(h)) == nil || c->type != GCommit) + sysfatal("could not read HEAD %H", h); + if((t = readobject(c->commit->tree)) == nil) + sysfatal("could not read tree for commit %H", h); + return t; +} + +void +usage(void) +{ + fprint(2, "usage: %s -n name -e email -m message -d date [files...]\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char *ln, *dstr, *parts[4], cwd[1024]; + int i, r, line, ncwd; + Hash th, ch; + vlong date; + Biobuf *f; + Object *t; + + gitinit(nil, 0, nil); + if(access(".git", AEXIST) != 0) + sysfatal("could not find git repo: %r"); + if(getwd(cwd, sizeof(cwd)) == nil) + sysfatal("getcwd: %r"); + dstr = nil; + date = time(nil); + ncwd = strlen(cwd); + + ARGBEGIN{ + case 'm': + commitmsg = EARGF(usage()); + break; + case 'n': + authorname = EARGF(usage()); + break; + case 'e': + authoremail = EARGF(usage()); + break; + case 'N': + committername = EARGF(usage()); + break; + case 'E': + committeremail = EARGF(usage()); + break; + case 'd': + dstr = EARGF(usage()); + break; + case 'p': + if(nparents >= Maxparents) + sysfatal("too many parents"); + if(resolveref(&parents[nparents++], EARGF(usage())) == -1) + sysfatal("invalid parent: %r"); + break; + default: + usage(); + break; + }ARGEND; + + if(commitmsg == nil) + sysfatal("missing message"); + if(authorname == nil) + sysfatal("missing name"); + if(authoremail == nil) + sysfatal("missing email"); + if((committername == nil) != (committeremail == nil)) + sysfatal("partially specified committer"); + if(committername == nil && committeremail == nil){ + committername = authorname; + committeremail = authoremail; + } + if(dstr){ + date=strtoll(dstr, &dstr, 10); + if(strlen(dstr) != 0) + sysfatal("could not parse date %s", dstr); + } + for(i = 0; i < argc; i++){ + cleanname(argv[i]); + if(*argv[i] == '/' && strncmp(argv[i], cwd, ncwd) == 0) + argv[i] += ncwd; + while(*argv[i] == '/') + argv[i]++; + } + qsort(argv, argc, sizeof(*argv), namecmp); + + t = findroot(); + nidx = 0; + idxsz = 32; + idx = emalloc(idxsz*sizeof(Idxent)); + if((f = Bopen(".git/INDEX9", OREAD)) == nil) + sysfatal("open index: %r"); + line = 0; + while((ln = Brdstr(f, '\n', 1)) != nil){ + line++; + if(ln[0] == 0 || ln[0] == '\n') + continue; + if(getfields(ln, parts, nelem(parts), 0, " \t") != nelem(parts)) + sysfatal(".git/INDEX9:%d: corrupt index", line); + if(nidx == idxsz){ + idxsz += idxsz/2; + idx = realloc(idx, idxsz*sizeof(Idxent)); + } + cleanname(parts[3]); + idx[nidx].state = *parts[0]; + idx[nidx].qid = parseqid(parts[1]); + idx[nidx].mode = strtol(parts[2], nil, 8); + idx[nidx].path = strdup(parts[3]); + idx[nidx].order = nidx; + nidx++; + free(ln); + } + Bterm(f); + qsort(idx, nidx, sizeof(Idxent), idxcmp); + r = treeify(t, argv, argv + argc, 0, &th); + if(r == -1) + sysfatal("could not commit: %r"); + mkcommit(&ch, date, th); + print("%H\n", ch); + exits(nil); +} --- /dev/null +++ /sys/src/cmd/git/send.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,280 @@ +#include +#include + +#include "git.h" + +typedef struct Map Map; + +struct Map { + char *ref; + Hash ours; + Hash theirs; +}; + +int sendall; +int force; +int nbranch; +char **branch; +char *removed[128]; +int nremoved; +int npacked; +int nsent; + +int +findref(char **r, int nr, char *ref) +{ + int i; + for(i = 0; i < nr; i++) + if(strcmp(r[i], ref) == 0) + return i; + return -1; +} + +int +findkey(Map *m, int nm, char *ref) +{ + int i; + for(i = 0; i < nm; i++) + if(strcmp(m[i].ref, ref) == 0) + return i; + return -1; +} + +int +readours(Hash **tailp, char ***refp) +{ + int nu, i, idx; + char *r, *pfx, **ref; + Hash *tail; + + if(sendall) + return listrefs(tailp, refp); + nu = 0; + tail = eamalloc((nremoved + nbranch), sizeof(Hash)); + ref = eamalloc((nremoved + nbranch), sizeof(char*)); + for(i = 0; i < nbranch; i++){ + ref[nu] = estrdup(branch[i]); + if(resolveref(&tail[nu], branch[i]) == -1) + sysfatal("broken branch %s", branch[i]); + nu++; + } + for(i = 0; i < nremoved; i++){ + pfx = "refs/heads/"; + if(strstr(removed[i], "heads/") == removed[i]) + pfx = "refs/"; + if(strstr(removed[i], "refs/heads/") == removed[i]) + pfx = ""; + if((r = smprint("%s%s", pfx, removed[i])) == nil) + sysfatal("smprint: %r"); + if((idx = findref(ref, nu, r)) == -1) + idx = nu++; + else + free(ref[idx]); + assert(idx < nremoved + nbranch); + memcpy(&tail[idx], &Zhash, sizeof(Hash)); + ref[idx] = r; + } + dprint(1, "nu: %d\n", nu); + for(i = 0; i < nu; i++) + dprint(1, "update: %H %s\n", tail[i], ref[i]); + *tailp = tail; + *refp = ref; + return nu; +} + +int +sendpack(Conn *c) +{ + int i, n, idx, nsp, send, first; + int nours, ntheirs, nmap; + char buf[Pktmax], *sp[3]; + Hash h, *theirs, *ours; + Object *a, *b, *p, *o; + char **refs; + Map *map, *m; + + first = 1; + nours = readours(&ours, &refs); + theirs = nil; + ntheirs = 0; + nmap = nours; + map = eamalloc(nmap, sizeof(Map)); + for(i = 0; i < nmap; i++){ + map[i].theirs = Zhash; + map[i].ours = ours[i]; + map[i].ref = refs[i]; + } + while(1){ + n = readpkt(c, buf, sizeof(buf)); + if(n == -1) + return -1; + if(n == 0) + break; + if(first && n > strlen(buf)) + parsecaps(buf + strlen(buf) + 1, c); + first = 0; + + if(getfields(buf, sp, nelem(sp), 1, " \t\r\n") != 2) + sysfatal("invalid ref line %.*s", utfnlen(buf, n), buf); + theirs = earealloc(theirs, ntheirs+1, sizeof(Hash)); + if(hparse(&theirs[ntheirs], sp[0]) == -1) + sysfatal("invalid hash %s", sp[0]); + if((idx = findkey(map, nmap, sp[1])) != -1) + map[idx].theirs = theirs[ntheirs]; + /* + * we only keep their ref if we can read the object to add it + * to our reachability; otherwise, discard it; we only care + * that we don't have it, so we can tell whether we need to + * bail out of pushing. + */ + if((o = readobject(theirs[ntheirs])) != nil){ + ntheirs++; + unref(o); + } + } + + if(writephase(c) == -1) + return -1; + send = 0; + if(force) + send=1; + for(i = 0; i < nmap; i++){ + m = &map[i]; + a = readobject(m->theirs); + if(hasheq(&m->ours, &Zhash)) + b = nil; + else + b = readobject(m->ours); + p = nil; + if(a != nil && b != nil) + p = ancestor(a, b); + if(!force + && !hasheq(&m->theirs, &Zhash) + && !hasheq(&m->ours, &Zhash) + && (a == nil || p != a)){ + fprint(2, "remote has diverged\n"); + werrstr("remote diverged"); + flushpkt(c); + return -1; + } + unref(a); + unref(b); + unref(p); + if(hasheq(&m->theirs, &m->ours)){ + print("uptodate %s\n", m->ref); + continue; + } + print("update %s %H %H\n", m->ref, m->theirs, m->ours); + n = snprint(buf, sizeof(buf), "%H %H %s", m->theirs, m->ours, m->ref); + + /* + * Workaround for github. + * + * Github will accept the pack but fail to update the references + * if we don't have capabilities advertised. Report-status seems + * harmless to add, so we add it. + * + * Github doesn't advertise any capabilities, so we can't check + * for compatibility. We just need to add it blindly. + */ + if(i == 0 && c->report){ + buf[n++] = '\0'; + n += snprint(buf + n, sizeof(buf) - n, " report-status"); + } + if(writepkt(c, buf, n) == -1) + sysfatal("unable to send update pkt"); + send = 1; + } + flushpkt(c); + if(!send){ + fprint(2, "nothing to send\n"); + return 0; + } + + if(writepack(c->wfd, ours, nours, theirs, ntheirs, &h) == -1) + return -1; + if(!c->report) + return 0; + + if(readphase(c) == -1) + return -1; + /* We asked for a status report, may as well use it. */ + while((n = readpkt(c, buf, sizeof(buf))) > 0){ + buf[n] = 0; + if(chattygit) + fprint(2, "done sending pack, status %s\n", buf); + nsp = getfields(buf, sp, nelem(sp), 1, " \t\n\r"); + if(nsp < 2) + continue; + if(nsp < 3) + sp[2] = ""; + /* + * Only report errors; successes will be reported by + * surrounding scripts. + */ + if(strcmp(sp[0], "unpack") == 0 && strcmp(sp[1], "ok") != 0) + werrstr("unpack %s", sp[1]); + else if(strcmp(sp[0], "ng") == 0) + werrstr("failed update: %s %s", sp[1], sp[2]); + else + continue; + return -1; + } + return 0; +} + +void +usage(void) +{ + fprint(2, "usage: %s remote [reponame]\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char *br; + Conn c; + + ARGBEGIN{ + default: + usage(); + break; + case 'd': + chattygit++; + break; + case 'f': + force++; + break; + case 'r': + if(nremoved == nelem(removed)) + sysfatal("too many deleted branches"); + removed[nremoved++] = EARGF(usage()); + break; + case 'a': + sendall++; + break; + case 'b': + br = EARGF(usage()); + if(strncmp(br, "refs/heads/", strlen("refs/heads/")) == 0) + br = smprint("%s", br); + else if(strncmp(br, "heads/", strlen("heads/")) == 0) + br = smprint("refs/%s", br); + else + br = smprint("refs/heads/%s", br); + branch = erealloc(branch, (nbranch + 1)*sizeof(char*)); + branch[nbranch] = br; + nbranch++; + break; + }ARGEND; + + gitinit(nil, 0, nil); + if(argc != 1) + usage(); + if(gitconnect(&c, argv[0], "receive") == -1) + sysfatal("git connect: %s: %r", argv[0]); + if(sendpack(&c) == -1) + sysfatal("send failed: %r"); + closeconn(&c); + exits(nil); +} --- /dev/null +++ /sys/src/cmd/git/serve.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,606 @@ +#include +#include +#include +#include + +#include "git.h" + +char *pathpfx = nil; +int allowwrite; + +static void +fail(Conn *c, char *fmt, ...) +{ + char msg[ERRMAX]; + va_list ap; + + va_start(ap, fmt); + vsnprint(msg, sizeof(msg), fmt, ap); + va_end(ap); + fmtpkt(c, "ERR %s\n", msg); + sysfatal("%s", msg); +} + +char* +gethead(Hash *h, char *ref, int nref) +{ + int fd, n; + char *s; + + if((fd = open(".git/HEAD", OREAD)) == -1) + return nil; + if((n = readn(fd, ref, nref-1)) == -1) + return nil; + ref[n] = 0; + strip(ref); + if(strncmp(ref, "ref: ", 5) != 0) + return nil; + s = ref+5; + if(resolveref(h, s) == -1) + return nil; + return s; +} + +int +showrefs(Conn *c) +{ + char **names, *s, buf[256]; + int i, ret, nrefs; + Hash head, *refs; + + ret = -1; + nrefs = 0; + refs = nil; + names = nil; + + if((s = gethead(&head, buf, sizeof(buf))) == nil) + memset(&head, 0, sizeof(Hash)); + if(fmtpkt(c, "%H HEAD%csymref=HEAD:%s no-thin\n", head, 0, s) == -1) + goto error; + if((nrefs = listrefs(&refs, &names)) == -1) + fail(c, "listrefs: %r"); + for(i = 0; i < nrefs; i++){ + if(strncmp(names[i], "heads/", strlen("heads/")) != 0) + continue; + if(fmtpkt(c, "%H refs/%s\n", refs[i], names[i]) == -1) + goto error; + } + if(flushpkt(c) == -1) + goto error; + ret = 0; +error: + for(i = 0; i < nrefs; i++) + free(names[i]); + free(names); + free(refs); + return ret; +} + +int +servnegotiate(Conn *c, Hash **head, int *nhead, Hash **tail, int *ntail) +{ + char pkt[Pktmax]; + int n, acked; + Object *o; + Hash h; + + if(showrefs(c) == -1) + return -1; + + *head = nil; + *tail = nil; + *nhead = 0; + *ntail = 0; + while(1){ + if((n = readpkt(c, pkt, sizeof(pkt))) == -1) + goto error; + if(n == 0) + break; + if(strncmp(pkt, "want ", 5) != 0){ + werrstr(" protocol garble %s", pkt); + goto error; + } + if(hparse(&h, &pkt[5]) == -1){ + werrstr(" garbled want"); + goto error; + } + if((o = readobject(h)) == nil){ + werrstr("requested nonexistent object"); + goto error; + } + unref(o); + *head = erealloc(*head, (*nhead + 1)*sizeof(Hash)); + (*head)[*nhead] = h; + *nhead += 1; + } + + acked = 0; + while(1){ + if((n = readpkt(c, pkt, sizeof(pkt))) == -1) + goto error; + if(strncmp(pkt, "done", 4) == 0) + break; + if(n == 0){ + if(!acked && fmtpkt(c, "NAK") == -1) + goto error; + } + if(strncmp(pkt, "have ", 5) != 0){ + werrstr(" protocol garble %s", pkt); + goto error; + } + if(hparse(&h, &pkt[5]) == -1){ + werrstr(" garbled have"); + goto error; + } + if((o = readobject(h)) == nil) + continue; + if(!acked){ + if(fmtpkt(c, "ACK %H", h) == -1) + goto error; + acked = 1; + } + unref(o); + *tail = erealloc(*tail, (*ntail + 1)*sizeof(Hash)); + (*tail)[*ntail] = h; + *ntail += 1; + } + if(!acked && fmtpkt(c, "NAK\n") == -1) + goto error; + return 0; +error: + fmtpkt(c, "ERR %r\n"); + free(*head); + free(*tail); + return -1; +} + +int +servpack(Conn *c) +{ + Hash *head, *tail, h; + int nhead, ntail; + + dprint(1, "negotiating pack\n"); + if(servnegotiate(c, &head, &nhead, &tail, &ntail) == -1) + fail(c, "negotiate: %r"); + dprint(1, "writing pack\n"); + if(writepack(c->wfd, head, nhead, tail, ntail, &h) == -1) + fail(c, "send: %r"); + return 0; +} + +int +validref(char *s) +{ + cleanname(s); + if(strncmp(s, "refs/", 5) != 0) + return 0; + return okref(s); +} + +int +recvnegotiate(Conn *c, Hash **cur, Hash **upd, char ***ref, int *nupd) +{ + char pkt[Pktmax], refpath[512], *sp[4]; + Hash old, new; + int n, i; + + if(showrefs(c) == -1) + return -1; + *cur = nil; + *upd = nil; + *ref = nil; + *nupd = 0; + while(1){ + if((n = readpkt(c, pkt, sizeof(pkt))) == -1) + goto error; + if(n == 0) + break; + if(getfields(pkt, sp, nelem(sp), 1, " \t\n\r") != 3){ + fmtpkt(c, "ERR protocol garble %s\n", pkt); + goto error; + } + if(hparse(&old, sp[0]) == -1){ + fmtpkt(c, "ERR bad old hash %s\n", sp[0]); + goto error; + } + if(hparse(&new, sp[1]) == -1){ + fmtpkt(c, "ERR bad new hash %s\n", sp[1]); + goto error; + } + if(!validref(sp[2])){ + fmtpkt(c, "ERR invalid ref %s\n", sp[2]); + goto error; + } + *cur = erealloc(*cur, (*nupd + 1)*sizeof(Hash)); + *upd = erealloc(*upd, (*nupd + 1)*sizeof(Hash)); + *ref = erealloc(*ref, (*nupd + 1)*sizeof(Hash)); + (*cur)[*nupd] = old; + (*upd)[*nupd] = new; + (*ref)[*nupd] = estrdup(sp[2]); + n = snprint(refpath, sizeof(refpath), ".git/%s", sp[2]); + if(n >= sizeof(refpath)-1){ + fmtpkt(c, "ERR invalid ref %s\n", sp[2]); + goto error; + } + if(access(refpath, AWRITE) == -1 + && access(refpath, AEXIST) == 0){ + fmtpkt(c, "ERR read-only ref %s\n", sp[2]); + goto error; + } + *nupd += 1; + } + return 0; +error: + free(*cur); + free(*upd); + for(i = 0; i < *nupd; i++) + free((*ref)[i]); + free(*ref); + return -1; +} + +int +rename(char *pack, char *idx, Hash h) +{ + char name[128], path[196]; + Dir st; + + nulldir(&st); + st.name = name; + snprint(name, sizeof(name), "%H.pack", h); + snprint(path, sizeof(path), ".git/objects/pack/%s", name); + if(access(path, AEXIST) == 0) + fprint(2, "warning, pack %s already pushed\n", name); + else if(dirwstat(pack, &st) == -1) + return -1; + snprint(name, sizeof(name), "%H.idx", h); + snprint(path, sizeof(path), ".git/objects/pack/%s", name); + if(access(path, AEXIST) == 0) + fprint(2, "warning, pack %s already indexed\n", name); + else if(dirwstat(idx, &st) == -1) + return -1; + return 0; +} + +int +checkhash(int fd, vlong sz, Hash *hcomp) +{ + DigestState *st; + Hash hexpect; + char buf[Pktmax]; + vlong n, r; + int nr; + + if(sz < 28){ + werrstr("undersize packfile"); + return -1; + } + + st = nil; + n = 0; + if(seek(fd, 0, 0) == -1) + sysfatal("packfile seek: %r"); + while(n != sz - 20){ + nr = sizeof(buf); + if(sz - n - 20 < sizeof(buf)) + nr = sz - n - 20; + r = readn(fd, buf, nr); + if(r != nr){ + werrstr("short read"); + return -1; + } + st = sha1((uchar*)buf, nr, nil, st); + n += r; + } + sha1(nil, 0, hcomp->h, st); + if(readn(fd, hexpect.h, sizeof(hexpect.h)) != sizeof(hexpect.h)) + sysfatal("truncated packfile"); + if(!hasheq(hcomp, &hexpect)){ + werrstr("bad hash: %H != %H", *hcomp, hexpect); + return -1; + } + return 0; +} + +int +mkdir(char *dir) +{ + char buf[ERRMAX]; + int f; + + if(access(dir, AEXIST) == 0) + return 0; + if((f = create(dir, OREAD, DMDIR | 0755)) == -1){ + rerrstr(buf, sizeof(buf)); + if(strstr(buf, "exist") == nil) + return -1; + } + close(f); + return 0; +} + +int +mkpath(char *path) +{ + char *p; + + p = path; + /* + * we assume that the path has been + * checked with validref(), so there + * are no double '/' or odd characters. + */ + while(1){ + p = strchr(p, '/'); + if(p == nil) + return 0; + *p = 0; + if(mkdir(path) == -1) + return -1; + *p++ = '/'; + } +} + +int +updatepack(Conn *c) +{ + char buf[Pktmax], packtmp[128], idxtmp[128], ebuf[ERRMAX]; + int n, pfd, packsz; + Hash h; + + /* make sure the needed dirs exist */ + if(mkdir(".git/objects") == -1) + return -1; + if(mkdir(".git/objects/pack") == -1) + return -1; + if(mkdir(".git/refs") == -1) + return -1; + if(mkdir(".git/refs/heads") == -1) + return -1; + snprint(packtmp, sizeof(packtmp), ".git/objects/pack/recv-%d.pack.tmp", getpid()); + snprint(idxtmp, sizeof(idxtmp), ".git/objects/pack/recv-%d.idx.tmp", getpid()); + if((pfd = create(packtmp, ORDWR, 0644)) == -1) + return -1; + packsz = 0; + while(1){ + n = read(c->rfd, buf, sizeof(buf)); + if(n == 0) + break; + if(n == -1){ + rerrstr(ebuf, sizeof(ebuf)); + if(strstr(ebuf, "hungup") == nil) + return -1; + break; + } + if(write(pfd, buf, n) != n) + return -1; + packsz += n; + } + if(checkhash(pfd, packsz, &h) == -1){ + dprint(1, "hash mismatch\n"); + goto error1; + } + if(indexpack(packtmp, idxtmp, h) == -1){ + dprint(1, "indexing failed: %r\n"); + goto error1; + } + if(rename(packtmp, idxtmp, h) == -1){ + dprint(1, "rename failed: %r\n"); + goto error2; + } + return 0; + +error2: remove(idxtmp); +error1: remove(packtmp); + return -1; +} + +int +lockrepo(void) +{ + int fd, i; + + for(i = 0; i < 10; i++) { + if((fd = create(".git/_lock", ORCLOSE|ORDWR|OTRUNC|OEXCL, 0644))!= -1) + return fd; + sleep(250); + } + return -1; +} + +int +updaterefs(Conn *c, Hash *cur, Hash *upd, char **ref, int nupd) +{ + char refpath[512], buf[128]; + int i, newidx, hadref, fd, ret, lockfd; + vlong newtm; + Object *o; + Hash h; + + ret = -1; + hadref = 0; + newidx = -1; + /* + * Date of Magna Carta. + * Wrong because it was computed using + * the proleptic gregorian calendar. + */ + newtm = -23811206400; + if((lockfd = lockrepo()) == -1){ + snprint(buf, sizeof(buf), "repo locked\n"); + return -1; + } + for(i = 0; i < nupd; i++){ + if(resolveref(&h, ref[i]) == 0){ + hadref = 1; + if(!hasheq(&h, &cur[i])){ + snprint(buf, sizeof(buf), "old ref changed: %s", ref[i]); + goto error; + } + } + if(snprint(refpath, sizeof(refpath), ".git/%s", ref[i]) == sizeof(refpath)){ + snprint(buf, sizeof(buf), "ref path too long: %s", ref[i]); + goto error; + } + if(hasheq(&upd[i], &Zhash)){ + remove(refpath); + continue; + } + if((o = readobject(upd[i])) == nil){ + snprint(buf, sizeof(buf), "update to nonexistent hash %H", upd[i]); + goto error; + } + if(o->type != GCommit){ + snprint(buf, sizeof(buf), "not commit: %H", upd[i]); + goto error; + } + if(o->commit->mtime > newtm){ + newtm = o->commit->mtime; + newidx = i; + } + unref(o); + if(mkpath(refpath) == -1){ + snprint(buf, sizeof(buf), "create path: %r"); + goto error; + } + if((fd = create(refpath, OWRITE|OTRUNC, 0644)) == -1){ + snprint(buf, sizeof(buf), "open ref: %r"); + goto error; + } + if(fprint(fd, "%H", upd[i]) == -1){ + snprint(buf, sizeof(buf), "upate ref: %r"); + close(fd); + goto error; + } + close(fd); + } + /* + * Heuristic: + * If there are no valid refs, and HEAD is invalid, then + * pick the ref with the newest commits as the default + * branch. + * + * Several people have been caught out by pushing to + * a repo where HEAD named differently from what got + * pushed, and this is going to be more of a footgun + * when 'master', 'main', and 'front' are all in active + * use. This should make us pick a useful default in + * those cases, instead of silently failing. + */ + if(resolveref(&h, "HEAD") == -1 && hadref == 0 && newidx != -1){ + if((fd = create(".git/HEAD", OWRITE|OTRUNC, 0644)) == -1){ + snprint(buf, sizeof(buf), "open HEAD: %r"); + goto error; + } + if(fprint(fd, "ref: %s", ref[newidx]) == -1){ + snprint(buf, sizeof(buf), "write HEAD ref: %r"); + goto error; + } + close(fd); + } + ret = 0; +error: + if(ret != 0) + fmtpkt(c, "ERR %s", buf); + close(lockfd); + werrstr(buf); + return ret; +} + +int +recvpack(Conn *c) +{ + Hash *cur, *upd; + char **ref; + int nupd; + + if(!allowwrite) + fail(c, "read-only repo"); + if(recvnegotiate(c, &cur, &upd, &ref, &nupd) == -1) + fail(c, "negotiate refs: %r"); + if(nupd != 0 && updatepack(c) == -1) + sysfatal("update pack: %r"); + if(nupd != 0 && updaterefs(c, cur, upd, ref, nupd) == -1) + sysfatal("update refs: %r"); + return 0; +} + +char* +parsecmd(char *buf, char *cmd, int ncmd) +{ + int i; + char *p; + + for(p = buf, i = 0; *p && i < ncmd - 1; i++, p++){ + if(*p == ' ' || *p == '\t'){ + cmd[i] = 0; + break; + } + cmd[i] = *p; + } + while(*p == ' ' || *p == '\t') + p++; + return p; +} + +void +usage(void) +{ + fprint(2, "usage: %s [-dw] [-r rel]\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char *repo, cmd[32], buf[512]; + Conn c; + + ARGBEGIN{ + case 'd': + chattygit++; + break; + case 'r': + pathpfx = EARGF(usage()); + if(*pathpfx != '/') + sysfatal("path prefix must begin with '/'"); + break; + case 'w': + allowwrite++; + break; + default: + usage(); + break; + }ARGEND; + + interactive = 0; + if(rfork(RFNAMEG) == -1) + sysfatal("rfork: %r"); + if(pathpfx != nil){ + if(bind(pathpfx, "/", MREPL) == -1) + sysfatal("bind: %r"); + } + if(rfork(RFNOMNT) == -1) + sysfatal("rfork: %r"); + + initconn(&c, 0, 1); + if(readpkt(&c, buf, sizeof(buf)) == -1) + sysfatal("readpkt: %r"); + repo = parsecmd(buf, cmd, sizeof(cmd)); + cleanname(repo); + if(strncmp(repo, "../", 3) == 0) + fail(&c, "invalid path %s\n", repo); + if(bind(repo, "/", MREPL) == -1) + fail(&c, "no such repo: %s", repo); + if(chdir("/") == -1) + fail(&c, "no such repo"); + gitinit(nil, 0, nil); + if(strcmp(cmd, "git-receive-pack") == 0) + recvpack(&c); + else if(strcmp(cmd, "git-upload-pack") == 0) + servpack(&c); + else + fail(&c, "unsupported command '%s'", cmd); + exits(nil); +} --- /dev/null +++ /sys/src/cmd/git/test/add.expected Fri Jun 19 00:00:00 2026 @@ -0,0 +1,9 @@ +A a +A b +A dir/a +A dir/b +A extra/a +A extra/b +A more/a +A more/b +A more/c --- /dev/null +++ /sys/src/cmd/git/test/add.rc Fri Jun 19 00:00:00 2026 @@ -0,0 +1,34 @@ +#!/bin/rc + +. ./util.rc + +rm -fr scratch +mkdir -p scratch/repo + +echo @@ abs paths @@ +@{ + rfork ne + cd scratch/repo + pwd=`{pwd} + git/init + mkdir dir + mkdir another + mkdir more + mkdir extra + touch a b c + touch dir/^(a b c) + touch another/^(a b c) + touch more/^(a b c) + touch extra/^(a b c) + git/add a + git/add $pwd/b + git/add dir/a + git/add $pwd/dir/b + git/add more + @{cd more && git/add ../extra/a} + @{cd more && git/add $pwd/extra/b} + git/fs + git/walk > ../added +} + +diff -c scratch/added add.expected >/dev/null || die wrong files --- /dev/null +++ /sys/src/cmd/git/test/basic.rc Fri Jun 19 00:00:00 2026 @@ -0,0 +1,66 @@ +#!/bin/rc + +. ./util.rc + +wrkdir=`{pwd} +rm -fr scratch +mkdir -p scratch/upstream + +echo @@ version1 @@ +@{ + cd scratch/upstream + q git/init + echo version1 > file.txt + q git/add file.txt + q git/commit -m version1 file.txt +} + +@{ + cd scratch + git/clone $wrkdir/scratch/upstream downstream +} + +diff -c scratch/upstream/file.txt scratch/downstream/file.txt || die mismatch + +echo @@ version2 @@ +@{ + cd scratch/upstream + echo version2 > file.txt + q git/commit -m version2 file.txt +} +@{ + cd scratch/downstream + q git/pull +} +q diff -c scratch/upstream/file.txt scratch/downstream/file.txt || die mismatch + +echo @@ version3 @@ +@{ + cd scratch/upstream + echo version3 > file2.txt + git/add file2.txt + q git/commit -m version3 file2.txt +} +@{ + cd scratch/downstream + q git/pull +} +q diff -c scratch/upstream/file.txt scratch/downstream/file.txt || die mismatch +q diff -c scratch/upstream/file2.txt scratch/downstream/file2.txt || die mismatch + +echo @@ version4 @@ +@{ + cd scratch/upstream + echo version4 > file.txt + git/rm file2.txt + rm file2.txt + q git/commit -m version4 file.txt file2.txt +} + +@{ + cd scratch/downstream + q git/pull +} +q diff -c scratch/upstream/file.txt scratch/downstream/file.txt || die mismatch +! test -e scratch/upstream/file2.txt || die mismatch +! test -e scratch/downstream/file2.txt || die mismatch --- /dev/null +++ /sys/src/cmd/git/test/diff.rc Fri Jun 19 00:00:00 2026 @@ -0,0 +1,46 @@ +#!/bin/rc + +. ./util.rc + +nl=' +' +rm -fr scratch +mkdir -p scratch/subdir/subdir2 +mkdir -p scratch/subdir3 + +echo @@git diff -s relative@@ +@{ + cd scratch + q git/init + echo hello > file.txt + echo hello1 > subdir/file1.txt + echo hello2 > subdir/subdir2/file2.txt + echo hello3 > subdir3/file3.txt + q git/add file.txt subdir/file1.txt subdir/subdir2/file2.txt subdir3/file3.txt + q git/commit -m initial . + echo >file.txt + echo >subdir/file1.txt + echo >subdir/subdir2/file2.txt + echo >subdir3/file3.txt + + out=`$nl{git/diff -s . | awk '{ print $2 }'} + ~ $out(1) file.txt && ~ $out(2) subdir/file1.txt && ~ $out(3) subdir/subdir2/file2.txt \ + ~ $out(4) subdir3/file3.txt || die 'base level fail' + + cd subdir + out=`$nl{git/diff -s .. | awk '{ print $2 }'} + ~ $out(1) ../file.txt && ~ $out(2) file1.txt && ~ $out(3) subdir2/file2.txt \ + ~ $out(4) ../subdir3/file3.txt || die 'subdir1 level fail' + + cd subdir2 + out=`$nl{git/diff -s ../.. | awk '{ print $2 }'} + ~ $out(1) ../../file.txt && ~ $out(2) ../file1.txt && ~ $out(3) file2.txt \ + ~ $out(4) ../../subdir3/file3.txt || die 'subdir2 level fail' + + cd ../../subdir3 + out=`$nl{git/diff -s .. | awk '{ print $2 }'} + ~ $out(1) ../file.txt && ~ $out(2) ../subdir/file1.txt && ~ $out(3) ../subdir/subdir2/file2.txt \ + ~ $out(4) file3.txt || die 'subdir3 level fail' + + ! git/diff -s ../.. >[2]/dev/null +} --- /dev/null +++ /sys/src/cmd/git/test/export.rc Fri Jun 19 00:00:00 2026 @@ -0,0 +1,40 @@ +#!/bin/rc + +. ./util.rc + +rm -fr scratch +mkdir -p scratch +cd scratch + +# setup test repo +@{ + rfork ne + q git/init a + + cd a + echo hello > a + echo goodbye > b + q git/add a b + q git/commit -m v1 . + cd .. + pwd + q git/clone `{pwd}^/a b +} + +echo @@ export and apply @@ +@{ + rfork ne + @{ + cd b + echo hihi > b + q git/commit -m export1 b + git/export > ../export1.patch + } + + @{ + cd a + q git/import ../export1.patch + } +} + +~ `{cd a && git/query HEAD} `{cd b && git/query HEAD} || die 'mismatched export' --- /dev/null +++ /sys/src/cmd/git/test/ftype.rc Fri Jun 19 00:00:00 2026 @@ -0,0 +1,52 @@ +#!/bin/rc + +. ./util.rc + +rm -fr scratch +mkdir -p scratch/repo1 + +echo @@ file-type change @@ +@{ + rfork ne + cd scratch/repo1 + repo1=`{pwd} + q git/init + + # in repo1: + # create A as a file + # create B as a directory + echo A > A + mkdir B + echo C > B/C + q git/add A B/C + q git/commit -m 1 A B/C + + cd .. + q git/clone $repo1 repo2 + cd repo2 + repo2=`{pwd} + + diff -r $repo1/A $repo2/A || exit 'clone fail A' + diff -r $repo1/B $repo2/B || exit 'clone fail B' + + cd $repo1 + + # in repo1: + # make A into a directory + # make B into a file + rm -r A B + mkdir A + echo B > A/B + echo B > B + q git/add A/B B + q git/commit -m 2 A/B B + + # pull repo2 after file changed to directory in repo1 + cd $repo2 + q git/pull + + diff -r $repo1/A $repo2/A || exit 'pull fail A' + diff -r $repo1/B $repo2/B || exit 'pull fail B' + + exit '' +} --- /dev/null +++ /sys/src/cmd/git/test/lca.rc Fri Jun 19 00:00:00 2026 @@ -0,0 +1,68 @@ +#!/bin/rc + +. ./util.rc + +rm -fr scratch +mkdir -p scratch +cd scratch + +echo @@ test lca @@ +@{ + q git/init a + + cd a + + echo 'first' > f + q git/add f + q git/commit -m base f + r=`{git/query HEAD} + + echo 0 > f + q git/commit -m a.0 . + a=`{git/query HEAD} + + for(i in `{seq 10}){ + echo $i > f + q git/commit -m a.$i . + } + + q git/branch -nb $r merge + echo x > f + q git/commit -m b.0 . + b=`{git/query HEAD} + + qq git/merge front + q git/commit -m merge . + m=`{git/query HEAD} + +~ `{git/query $a $m @} $a || die lca a-m +~ `{git/query $a $b @} $r || die lca a-b +~ `{git/query $a $r @} $r || die lca a-r +} + +# a +# ↓ +# b→c→d→e→f +# +# date order (oldest to newest): f d c b e a +echo @@ test lca rebase @@ +@{ + q git/init b + cd b + + touch f + + fn commit { + git/save -n regress -e regress $* f + } + + f=`{commit -m f -d 1} + e=`{commit -m e -d 5 -p $f} + d=`{commit -m d -d 2 -p $e} + c=`{commit -m c -d 3 -p $d} + b=`{commit -m b -d 4 -p $c} + a=`{commit -m a -d 6 -p $e} + +~ `{git/query $a $b @} $e || die lca a-b +~ `{git/query $b $a @} $e || die lca b-a +} --- /dev/null +++ /sys/src/cmd/git/test/merge.rc Fri Jun 19 00:00:00 2026 @@ -0,0 +1,123 @@ +#!/bin/rc + +. ./util.rc + +rm -fr scratch +mkdir -p scratch +cd scratch +c='foo +bar +baz +' + +# setup test repo +@{ + rfork ne + q git/init a + + cd a + echo hello > a + echo goodbye > b + echo -n $c > c + chmod +x a + q git/add a b c + q git/commit -m v1 . + cd .. + pwd + q git/clone `{pwd}^/a b +} + +echo @@ merge different files @@ +@{ + rfork ne + @{ + cd a + echo x > a + q git/commit -m diverge1a a + } + @{ + cd b + echo y > b + q git/commit -m diverge1b b + } + + @{ + cd b + qq git/pull + q git/merge origin/front || status='' + q git/commit -m merged + } +} + +flag +x +~ `{cat b/a} x || die merge 1.a +~ `{cat b/b} y || die merge 1.b +~ `''{cat b/c} $c || die merge 1.c +test -x b/a || die merge preserve exec +! test -x b/b || die merge preserve nonexec b +! test -x b/c || die merge preserve nonexec c +@{rfork ne ; cd b ; git/fs; git/walk -q} || die merge commit dropped files + +echo @@ merge concurent edits @@ +@{ + rfork ne + @{ + cd a + chmod -x a + chmod +x b + echo quux >>c + q git/commit -m diverge2a a b c + } + @{ + cd b + sed s/foo/FOO/ c.new + mv c.new c + q git/commit -m diverge2b c + + qq git/pull + qq git/merge origin/front + q git/commit -m merge c + } +} + +c='FOO +bar +baz +quux +' +~ `{cat b/a} x || die # commit from a +~ `{cat b/b} y || die # commit from b +~ `''{cat b/c} $c || {diff -u b/c <{echo $c}; die merge 1.c} +! test -x b/a || die merge remove exec +test -x b/b || die merge add exec +! test -x b/c || die merge preserve nonexec c + +# repro for bug in git/save, around missing +# files when saving the merged commit. +mkdir -p scratch/james +echo @@ james test @@ +@{ + cd scratch/james + q git/init + mkdir -p lib/ndb + touch lib/words + q git/add lib/words + q git/commit -m 'add words' lib/words + q git/branch -n myhead + echo stuff > lib/ndb/local + q git/add lib/ndb/local + q git/commit -m 'Add lib/ndb/local' lib/ndb/local + + q git/branch front + echo cromulent >> lib/words + q git/commit -m 'Some change on front' lib/words + q git/branch myhead + q git/merge front + q git/commit -m 'Merge front' + d=`''{git/diff lib/words lib/ndb/local} # should have no output + if(! ~ $d ''){ + echo $d + exit diff + } + exit '' +} --- /dev/null +++ /sys/src/cmd/git/test/mkfile Fri Jun 19 00:00:00 2026 @@ -0,0 +1,24 @@ +a; git/add a; git/commit -m a a + + q git/branch -n front-test + echo c >c; + q git/add c + q git/commit -m c c + + q git/branch front + echo b >b; + q git/add b + q git/commit -m b b + + git/branch front-test + git/rebase front | rc +} --- /dev/null +++ /sys/src/cmd/git/test/util.rc Fri Jun 19 00:00:00 2026 @@ -0,0 +1,18 @@ +fn q { + {$* > /tmp/out.$pid && rm /tmp/out.$pid} || cat /tmp/out.$pid +} +fn qq { + $* >/dev/null >[2]/dev/null +} + +fn die { + st=$status + if(! ~ $st ''){ + *=($* : $st) + echo $"* + exit $"* + } +} + +rfork n +bind $cputype.git /bin/git --- /dev/null +++ /sys/src/cmd/git/util.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,565 @@ +#include +#include +#include + +#include "git.h" + +Reprog *authorpat; +Hash Zhash; +int chattygit; +int interactive = 1; +int gitdirmode = -1; + +enum { + Seed = 2928213749ULL +}; + +Object* +emptydir(void) +{ + static Object *e; + + if(e != nil) + return ref(e); + e = emalloc(sizeof(Object)); + e->hash = Zhash; + e->type = GTree; + e->tree = emalloc(sizeof(Tinfo)); + e->tree->ent = nil; + e->tree->nent = 0; + e->flag |= Cloaded|Cparsed; + e->off = -1; + ref(e); + cache(e); + return e; +} +int +entcmp(void *pa, void *pb) +{ + Dirent *ae, *be; + uchar *a, *b; + int ca, cb; + + ae = pa; + be = pb; + a = (uchar*)ae->name; + b = (uchar*)be->name; + /* + * If the files have the same name, they're equal. + * Otherwise, If they're trees, they sort as thoug + * there was a trailing slash. + * + * Wat. + */ + while(1){ + ca = *a++; + cb = *b++; + /* + * because these are dir entries in a tree, + * the only '/' allowable is the virtual '/' + * at the end of the file name. + */ + assert(ca != '/' && cb != '/'); + if(ca != cb){ + if(ca == 0 && (ae->mode & DMDIR)) + ca = '/'; + if(cb == 0 && (be->mode & DMDIR)) + cb = '/'; + return (ca > cb) ? 1 : -1; + } + if(ca == 0){ + if(ae->mode & DMDIR) + ca = '/'; + if(be->mode & DMDIR) + cb = '/'; + if(ca == cb) + return 0; + return (ca > cb) ? 1 : -1; + } + } +} + +int +hasheq(Hash *a, Hash *b) +{ + return memcmp(a->h, b->h, sizeof(a->h)) == 0; +} + +int +charval(int c) +{ + if(c >= '0' && c <= '9') + return c - '0'; + if(c >= 'a' && c <= 'f') + return c - 'a' + 10; + if(c >= 'A' && c <= 'F') + return c - 'A' + 10; + werrstr("invalid hex char"); + return -1; +} + +void * +emalloc(ulong n) +{ + void *v; + + v = mallocz(n, 1); + if(v == nil) + sysfatal("malloc: %r"); + setmalloctag(v, getcallerpc(&n)); + return v; +} + +void * +eamalloc(ulong n, ulong sz) +{ + uvlong na; + void *v; + + na = (uvlong)n*(uvlong)sz; + if(na >= (1ULL<<30)) + sysfatal("alloc: overflow"); + v = mallocz(na, 1); + if(v == nil) + sysfatal("malloc: %r"); + setmalloctag(v, getcallerpc(&n)); + return v; +} + +void * +erealloc(void *p, ulong n) +{ + void *v; + + v = realloc(p, n); + if(v == nil) + sysfatal("realloc: %r"); + setmalloctag(v, getcallerpc(&p)); + return v; +} + +void * +earealloc(void *p, ulong n, ulong sz) +{ + uvlong na; + void *v; + + na = (uvlong)n*(uvlong)sz; + if(na >= (1ULL<<30)) + sysfatal("alloc: overflow"); + v = realloc(p, na); + if(v == nil) + sysfatal("realloc: %r"); + setmalloctag(v, getcallerpc(&p)); + return v; +} + +char* +estrdup(char *s) +{ + s = strdup(s); + if(s == nil) + sysfatal("strdup: %r"); + setmalloctag(s, getcallerpc(&s)); + return s; +} + +int +Hfmt(Fmt *fmt) +{ + Hash h; + int i, n, l; + char c0, c1; + + l = 0; + h = va_arg(fmt->args, Hash); + for(i = 0; i < sizeof h.h; i++){ + n = (h.h[i] >> 4) & 0xf; + c0 = (n >= 10) ? n-10 + 'a' : n + '0'; + n = h.h[i] & 0xf; + c1 = (n >= 10) ? n-10 + 'a' : n + '0'; + l += fmtprint(fmt, "%c%c", c0, c1); + } + return l; +} + +int +Tfmt(Fmt *fmt) +{ + int t; + int l; + + t = va_arg(fmt->args, int); + switch(t){ + case GNone: l = fmtprint(fmt, "none"); break; + case GCommit: l = fmtprint(fmt, "commit"); break; + case GTree: l = fmtprint(fmt, "tree"); break; + case GBlob: l = fmtprint(fmt, "blob"); break; + case GTag: l = fmtprint(fmt, "tag"); break; + case GOdelta: l = fmtprint(fmt, "odelta"); break; + case GRdelta: l = fmtprint(fmt, "gdelta"); break; + default: l = fmtprint(fmt, "?%d?", t); break; + } + return l; +} + +int +Ofmt(Fmt *fmt) +{ + Object *o; + int l; + + o = va_arg(fmt->args, Object *); + print("== %H (%T) ==\n", o->hash, o->type); + switch(o->type){ + case GTree: + l = fmtprint(fmt, "tree\n"); + break; + case GBlob: + l = fmtprint(fmt, "blob %s\n", o->data); + break; + case GCommit: + l = fmtprint(fmt, "commit\n"); + break; + case GTag: + l = fmtprint(fmt, "tag\n"); + break; + default: + l = fmtprint(fmt, "invalid: %d\n", o->type); + break; + } + return l; +} + +int +Qfmt(Fmt *fmt) +{ + Qid q; + + q = va_arg(fmt->args, Qid); + if(q.path == ~0ULL && q.vers == ~0UL && q.type == 0xff) + return fmtprint(fmt, "NOQID"); + else + return fmtprint(fmt, "%llux.%lud.%hhx", q.path, q.vers, q.type); +} + +/* Finds the directory containing the git repo. */ +static void +findrepo(char *buf, int nbuf, int *nrel) +{ + char *p, *suff; + + suff = "/.git/HEAD"; + if(getwd(buf, nbuf - strlen(suff) - 1) == nil) + sysfatal("getwd: %r"); + + *nrel = 0; + for(p = buf + strlen(buf); p != nil; p = strrchr(buf, '/')){ + strcpy(p, suff); + if(access(buf, AEXIST) == 0){ + p[p == buf] = '\0'; + return; + } + *nrel += 1; + *p = '\0'; + } + sysfatal("not a git repository"); +} + +void +gitinit(char *root, int nroot, int *nrel) +{ + char repo[512] = ".git"; + Dir *d; + + fmtinstall('H', Hfmt); + fmtinstall('T', Tfmt); + fmtinstall('O', Ofmt); + fmtinstall('Q', Qfmt); + inflateinit(); + deflateinit(); + authorpat = regcomp("[\t ]*(.*)[\t ]+([0-9]+)[\t ]*([\\-+]?[0-9]+)?"); + osinit(&objcache); + if(root != nil){ + findrepo(root, nroot, nrel); + snprint(repo, sizeof(repo), "%s/.git", root); + } + if((d = dirstat(repo)) == nil) + sysfatal("stat %s: %r", repo); + gitdirmode = d->mode & 0777; + free(d); +} + +int +hparse(Hash *h, char *b) +{ + int i, c0, c1; + + for(i = 0; i < nelem(h->h); i++){ + if((c0 = charval(b[2*i+0])) == -1) + return -1; + if((c1 = charval(b[2*i+1])) == -1) + return -1; + h->h[i] = (c0 << 4) | c1; + } + return 0; +} + +int +slurpdir(char *p, Dir **d) +{ + int r, f; + + if((f = open(p, OREAD)) == -1) + return -1; + r = dirreadall(f, d); + close(f); + return r; +} + +int +hassuffix(char *base, char *suf) +{ + int nb, ns; + + nb = strlen(base); + ns = strlen(suf); + if(ns <= nb && strcmp(base + (nb - ns), suf) == 0) + return 1; + return 0; +} + +int +swapsuffix(char *dst, int dstsz, char *base, char *oldsuf, char *suf) +{ + int bl, ol, sl, l; + + bl = strlen(base); + ol = strlen(oldsuf); + sl = strlen(suf); + l = bl + sl - ol; + if(l + 1 > dstsz || ol > bl) + return -1; + memmove(dst, base, bl - ol); + memmove(dst + bl - ol, suf, sl); + dst[l] = 0; + return l; +} + +char * +strip(char *s) +{ + char *e; + + while(isspace(*s)) + s++; + e = s + strlen(s); + while(e > s && isspace(*--e)) + *e = 0; + return s; +} + +void +_dprint(char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprint(2, fmt, ap); + va_end(ap); +} + +int +showprogress(int x, int pct) +{ + if(!interactive) + return 0; + if(x > pct){ + pct = x; + fprint(2, "\b\b\b\b%3d%%", pct); + } + return pct; +} + +void +qinit(Objq *q) +{ + memset(q, 0, sizeof(Objq)); + q->nheap = 0; + q->heapsz = 8; + q->heap = eamalloc(q->heapsz, sizeof(Qelt)); +} + +void +qclear(Objq *q) +{ + free(q->heap); +} + +void +qput(Objq *q, Object *o, int color) +{ + Qelt t; + int i; + + assert(o->type == GCommit); + if(q->nheap == q->heapsz){ + q->heapsz *= 2; + q->heap = earealloc(q->heap, q->heapsz, sizeof(Qelt)); + } + q->heap[q->nheap].o = o; + q->heap[q->nheap].color = color; + q->heap[q->nheap].ctime = o->commit->ctime; + for(i = q->nheap; i > 0; i = (i-1)/2){ + if(q->heap[i].ctime < q->heap[(i-1)/2].ctime) + break; + t = q->heap[i]; + q->heap[i] = q->heap[(i-1)/2]; + q->heap[(i-1)/2] = t; + } + q->nheap++; +} + +int +qpop(Objq *q, Qelt *e) +{ + int i, l, r, m; + Qelt t; + + if(q->nheap == 0) + return 0; + *e = q->heap[0]; + if(--q->nheap == 0) + return 1; + + i = 0; + q->heap[0] = q->heap[q->nheap]; + while(1){ + m = i; + l = 2*i+1; + r = 2*i+2; + if(l < q->nheap && q->heap[m].ctime < q->heap[l].ctime) + m = l; + if(r < q->nheap && q->heap[m].ctime < q->heap[r].ctime) + m = r; + if(m == i) + break; + t = q->heap[m]; + q->heap[m] = q->heap[i]; + q->heap[i] = t; + i = m; + } + return 1; +} + +u64int +murmurhash2(void *pp, usize n) +{ + u32int m = 0x5bd1e995; + u32int r = 24; + u32int h, k; + uchar *w, *e; + + h = Seed ^ n; + e = pp; + e += n & -4; + for (w = pp; w != e; w += 4) { + k = (u32int)w[0] | (u32int)w[1] << 8 | (u32int)w[2] << 16 | (u32int)w[3] << 24; + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + } + + switch (n & 0x3) { + case 3: h ^= w[2] << 16; + case 2: h ^= w[1] << 8; + case 1: h ^= w[0] << 0; + h *= m; + } + + h ^= h >> 13; + h *= m; + h ^= h >> 15; + + return h; +} + +Qid +parseqid(char *s) +{ + char *e; + Qid q; + + if(strcmp(s, "NOQID") == 0) + return (Qid){-1, -1, -1}; + e = s; + q.path = strtoull(e, &e, 16); + if(*e != '.') + sysfatal("corrupt qid: %s (%s)", s, e); + q.vers = strtoul(e+1, &e, 10); + if(*e != '.') + sysfatal("corrupt qid: %s (%s)", s, e); + q.type = strtoul(e+1, &e, 16); + if(*e != '\0') + sysfatal("corrupt qid: %s (%x)", s, *e); + return q; +} + +/* + * Checks the rules for valid ref names, as defined in + * git/Documentation/protocol-common.txt. + * It does not check that the ref begins with refs/ + * or is called HEAD, since that's only needed in the + * clone protocol. + */ +int +okref(char *ref) +{ + int n, slashed; + char *p; + Rune r; + + slashed = 0; + if(*ref == '/' || *ref == '.') + return 0; + for(p = ref; *p != 0; p += n) { + n = chartorune(&r, p); + switch(r){ + case '.': + if(p[1]== 0 || p[1] == '.') + return 0; + if(strcmp(p, ".lock") == 0) + return 0; + break; + case '/': + if(p[1] == 0 || p[1] == '.' || p[1] == '/') + return 0; + slashed = 1; + break; + case '@': + if(p[1] == '{') + return 0; + break; + case ' ': + case '~': + case '^': + case ':': + case '?': + case '*': + case '[': + case '\\': + case Runeerror: + case 0x7f: /* DEL */ + return 0; + default: + if(r < 0x20 || isspacerune(r)) + return 0; + } + } + if(ref == p) + return 0; + return slashed; +} --- /dev/null +++ /sys/src/cmd/git/walk.c Fri Jun 19 00:00:00 2026 @@ -0,0 +1,631 @@ +#include +#include +#include "git.h" + +typedef struct Idxed Idxed; +typedef struct Idxent Idxent; + +#define NCACHE 4096 + +enum { + Rflg = 1 << 0, + Mflg = 1 << 1, + Aflg = 1 << 2, + Uflg = 1 << 3, + /* everything after this is not an error */ + Tflg = 1 << 4, +}; + +struct Idxed { + char** cache; + int n; + int max; +}; + +Idxed idxtab[NCACHE]; +char repopath[1024]; +char wdirpath[1024]; +char relapath[1024]; +char *rstr = "R "; +char *mstr = "M "; +char *astr = "A "; +char *ustr = "U "; +char *tstr = "T "; +char *bdir = ".git/fs/HEAD/tree"; +int nslash; +int nrel; +int quiet; +int dirty; +int isindexed = 1; +int intree; +int printflg; + +Idxent *idx; +int idxsz; +int nidx; + +int cleanidx = 0; /* skip tree check for checkedin() */ +int staleidx = 0; + +Idxent *wdir; +int wdirsz; +int nwdir; + +void loadwdir(char*); + +int +checkedin(Idxent *e, int change) +{ + char *p; + int r; + + /* clean index, no need to check tree */ + if(cleanidx) + return e >= &idx[0] && e < &idx[nidx] && e->state != 'R'; + + if((p = smprint("%s/%s", bdir, e->path)) == nil) + sysfatal("smprint: %r"); + + r = access(p, AEXIST); + if(r == 0 && change){ + if(e->state != 'R') + e->state = 'T'; + staleidx = 1; + } + free(p); + + return r == 0; +} + +/* + * Tricky; we want to know if a file or dir is indexed, + * but a dir is only indexed if we have a file with dir/ + * listed in the index. + * + * as a result, we need to add a virtual '/' at the end + * of the path if we're doing it, so if we have: + * foo.bar/x + * foo/y + * and we want to find out if foo is a directory we should + * descend into, we need to compare as though foo/ ended + * with a '/', or we'll bsearch down do foo.bar, not foo. + * + * this code resembles entcmp() in util.c, but differs + * because we're comparing whole paths. + */ +int +pathcmp(char *sa, char *sb, int sadir) +{ + unsigned a, b; + + while(1){ + a = *sa++; + b = *sb++; + if(a != b){ + if(a == 0 && sadir) + a = '/'; + if(a == '/' && b == '/') + return 0; + return (a > b) ? 1 : -1; + } + if(a == 0) + return 0; + } +} + +int +indexed(char *path, int dir) +{ + int lo, hi, mid, r; + + r = -1; + lo = 0; + hi = nidx-1; + while(lo <= hi){ + mid = (hi + lo) / 2; + r = pathcmp(path, idx[mid].path, dir); + if(r < 0) + hi = mid-1; + else if(r > 0) + lo = mid+1; + else + break; + } + return r == 0; +} + +int +idxcmp(void *pa, void *pb) +{ + Idxent *a, *b; + int c; + + a = (Idxent*)pa; + b = (Idxent*)pb; + if((c = strcmp(a->path, b->path)) != 0) + return c; + /* maintain load order if name is identical */ + return a->order < b->order ? -1 : 1; +} + +/* + * compares whether the indexed entry 'a' + * has the same contents and mode as + * the entry on disk 'b'; if the indexed + * entry is nil, does a deep comparison + * of the checked out file and the file + * checked in. + */ +int +samedata(Idxent *a, Idxent *b) +{ + char *gitpath, ba[IOUNIT], bb[IOUNIT]; + int fa, fb, na, nb, same; + Dir *da, *db; + + if(a != nil){ + if(a->qid.path == b->qid.path + && a->qid.vers == b->qid.vers + && a->qid.type == b->qid.type + && a->mode == b->mode + && a->mode != 0) + return 1; + } + + same = 0; + da = nil; + db = nil; + if((gitpath = smprint("%s/%s", bdir, b->path)) == nil) + sysfatal("smprint: %r"); + fa = open(gitpath, OREAD); + fb = open(b->path, OREAD); + if(fa == -1 || fb == -1) + goto mismatch; + da = dirfstat(fa); + db = dirfstat(fb); + if(da == nil || db == nil) + goto mismatch; + if((da->mode&0100) != (db->mode&0100)) + goto mismatch; + if(da->length != db->length) + goto mismatch; + while(1){ + if((na = readn(fa, ba, sizeof(ba))) == -1) + goto mismatch; + if((nb = readn(fb, bb, sizeof(bb))) == -1) + goto mismatch; + if(na != nb) + goto mismatch; + if(na == 0) + break; + if(memcmp(ba, bb, na) != 0) + goto mismatch; + } + if(a != nil){ + a->qid = db->qid; + a->mode = db->mode; + staleidx = 1; + } + same = 1; + +mismatch: + free(da); + free(db); + if(fa != -1) + close(fa); + if(fb != -1) + close(fb); + return same; +} + +void +loadent(char *dir, Dir *d, int fullpath) +{ + char *path; + Idxent *e; + + if(fullpath) + path = estrdup(dir); + else if((path = smprint("%s/%s", dir, d->name)) == nil) + sysfatal("smprint: %r"); + + cleanname(path); + if(!intree && (printflg & Uflg) == 0 && !indexed(path, d->qid.type & QTDIR)){ + free(path); + return; + } + if(d->qid.type & QTDIR){ + loadwdir(path); + free(path); + }else{ + if(nwdir == wdirsz){ + wdirsz += wdirsz/2; + wdir = erealloc(wdir, wdirsz*sizeof(Idxent)); + } + e = wdir + nwdir; + e->path = path; + e->qid = !intree? d->qid : (Qid){-1,-1,-1}; + e->mode = d->mode; + e->order = nwdir; + e->state = 'T'; + nwdir++; + } +} + +void +loadwdir(char *path) +{ + int fd, i, n; + Dir *d, *e; + + d = nil; + e = nil; + cleanname(path); + if(!intree + && strncmp(path, ".git", 4) == 0 + && (path[4] == '/' || path[4] == 0)) + return; + + if((fd = open(path, OREAD)) < 0) + goto error; + if((e = dirfstat(fd)) == nil) + fprint(2, "fstat: %r"); + if(e->qid.type & QTDIR) + while((n = dirread(fd, &d)) > 0){ + for(i = 0; i < n; i++) + loadent(path, &d[i], 0); + free(d); + } + else + loadent(path, e, 1); +error: + free(e); + if(fd != -1) + close(fd); +} + +int +pfxmatch(char *p, char **pfx, int *pfxlen, int npfx) +{ + int i; + + if(p == nil) + return 0; + if(npfx == 0) + return 1; + for(i = 0; i < npfx; i++){ + if(strncmp(p, pfx[i], pfxlen[i]) != 0) + continue; + if(p[pfxlen[i]] == '/' || p[pfxlen[i]] == 0) + return 1; + if(strcmp(pfx[i], ".") == 0 || *pfx[i] == 0) + return 1; + } + return 0; +} + + +char* +reporel(char *s) +{ + char *p; + int n; + + if(*s == '/') + s = estrdup(s); + else if((s = smprint("%s/%s", wdirpath, s)) == nil) + sysfatal("smprint: %r"); + + p = cleanname(s); + n = strlen(repopath); + if(strncmp(s, repopath, n) != 0) + sysfatal("path outside repo: %s", s); + p += n; + if(*p == '/') + p++; + memmove(s, p, strlen(p)+1); + return s; +} + +void +show(Biobuf *o, int flg, char *str, char *path) +{ + char *pa, *pb; + int n; + + dirty |= flg; + if(!quiet && (printflg & flg)){ + Bprint(o, str); + n = nslash; + if(n){ + for(pa = relapath, pb = path; *pa && *pb; pa++, pb++){ + if(*pa != *pb) + break; + if(*pa == '/'){ + n--; + path = pb+1; + } + } + while(n-- > 0) + Bprint(o, "../"); + } + Bprint(o, "%s\n", path); + } +} + +void +findslashes(char *path) +{ + char *p; + + p = cleanname(path); + if(p[0] == '.'){ + if(p[1] == '\0') + return; + else if(p[1] == '.' && (p[2] == '/' || p[2] == '\0')) + sysfatal("relative path escapes git root"); + } + + snprint(relapath, sizeof relapath, "%s/", p); + p = relapath; + if(*p == '/') + p++; + + for(; *p; p++) + if(*p == '/') + nslash++; +} + +void +usage(void) +{ + fprint(2, "usage: %s [-qbcI] [-f filt] [-b base] [paths...]\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char *p, *e, *ln, *base, **argrel, *parts[4], xbuf[8]; + int i, j, c, line, wfd, *argn; + Biobuf *f, *o, *w; + Hash h; + Dir rn; + + gitinit(repopath, sizeof(repopath), &nrel); + if(getwd(wdirpath, sizeof(wdirpath)) == nil) + sysfatal("getwd: %r"); + if(chdir(repopath) == -1) + sysfatal("chdir: %r"); + if(access(".git/fs/ctl", AEXIST) != 0) + sysfatal("no running git/fs"); + ARGBEGIN{ + case 'q': + quiet++; + break; + case 'c': + rstr = ""; + tstr = ""; + mstr = ""; + astr = ""; + ustr = ""; + break; + case 'f': + for(p = EARGF(usage()); *p; p++) + switch(*p){ + case 'T': printflg |= Tflg; break; + case 'A': printflg |= Aflg; break; + case 'M': printflg |= Mflg; break; + case 'R': printflg |= Rflg; break; + case 'U': printflg |= Uflg; break; + default: usage(); break; + } + break; + case 'b': + isindexed = 0; + base = EARGF(usage()); + if(resolveref(&h, base) == -1) + sysfatal("no such ref '%s'", base); + bdir = smprint(".git/fs/object/%H/tree", h); + break; + case 'I': + /* invalidate index */ + staleidx = 1; + break; + case 'r': + findslashes(EARGF(usage())); + break; + default: + usage(); + }ARGEND; + + if(printflg == 0) + printflg = Tflg | Aflg | Mflg | Rflg; + + argrel = emalloc(argc*sizeof(char*)); + argn = emalloc(argc*sizeof(int)); + for(i = 0; i < argc; i++){ + argrel[i] = reporel(argv[i]); + argn[i] = strlen(argrel[i]); + } + + if(isindexed && !staleidx){ + if((f = Bopen(".git/INDEX9", OREAD)) == nil){ + if(access(".git/index9", AEXIST) == 0){ + fprint(2, "index format conversion needed:\n"); + fprint(2, "\tcd %s && git/fs\n", repopath); + fprint(2, "\t@{cd .git/index9/removed >[2]/dev/null && walk -f | sed 's/^/R NOQID 0 /'} >> .git/INDEX9\n"); + fprint(2, "\t@{cd .git/fs/HEAD/tree && walk -f | sed 's/^/T NOQID 0 /'} >> .git/INDEX9\n"); + exits("noindex"); + } + staleidx = 1; + goto Stale; + } + + nidx = 0; + idxsz = 32; + idx = emalloc(idxsz*sizeof(Idxent)); + + line = 0; + while((ln = Brdstr(f, '\n', 1)) != nil){ + line++; + /* allow blank lines */ + if(ln[0] == 0 || ln[0] == '\n') + continue; + if(getfields(ln, parts, nelem(parts), 0, " \t") != nelem(parts)) + sysfatal(".git/INDEX9:%d: corrupt index", line); + cleanname(parts[3]); + if(nidx == idxsz){ + idxsz += idxsz/2; + idx = erealloc(idx, idxsz*sizeof(Idxent)); + } + idx[nidx].state = *parts[0]; + idx[nidx].qid = parseqid(parts[1]); + idx[nidx].mode = strtol(parts[2], nil, 8); + idx[nidx].path = estrdup(parts[3]); + idx[nidx].order = nidx; + nidx++; + free(ln); + } + Bterm(f); + } else { +Stale: + nwdir = 0; + wdirsz = 32; + wdir = emalloc(wdirsz*sizeof(Idxent)); + + if(chdir(bdir) == -1) + sysfatal("chdir: %r"); + + /* load whole tree into index when stale */ + intree = 1; + if(staleidx || argc == 0) + loadwdir("."); + else for(i = 0; i < argc; i++) + loadwdir(argrel[i]); + + if(chdir(repopath) == -1) + sysfatal("chdir: %r"); + + /* use as index */ + idx = wdir; + nidx = nwdir; + idxsz = wdirsz; + + cleanidx = 1; + } + qsort(idx, nidx, sizeof(Idxent), idxcmp); + + nwdir = 0; + wdirsz = 32; + wdir = emalloc(wdirsz*sizeof(Idxent)); + + intree = 0; + if(argc == 0) + loadwdir("."); + else for(i = 0; i < argc; i++) + loadwdir(argrel[i]); + qsort(wdir, nwdir, sizeof(Idxent), idxcmp); + + if((o = Bfdopen(1, OWRITE)) == nil) + sysfatal("open out: %r"); + + i = 0; + j = 0; + while(i < nidx || j < nwdir){ + /* find the last entry we tracked for a path */ + while(i+1 < nidx && strcmp(idx[i].path, idx[i+1].path) == 0){ + staleidx = 1; + i++; + } + while(j+1 < nwdir && strcmp(wdir[j].path, wdir[j+1].path) == 0) + j++; + if(i < nidx && !pfxmatch(idx[i].path, argrel, argn, argc)){ + i++; + continue; + } + if(i >= nidx) + c = 1; + else if(j >= nwdir) + c = -1; + else + c = strcmp(idx[i].path, wdir[j].path); + /* exists in both index and on disk */ + if(c == 0){ + if(idx[i].state == 'R'){ + if(checkedin(&idx[i], 0)) + show(o, Rflg, rstr, idx[i].path); + else{ + idx[i].state = 'U'; + staleidx = 1; + } + }else if(idx[i].state == 'A' && !checkedin(&idx[i], 1)) + show(o, Aflg, astr, idx[i].path); + else if(!samedata(&idx[i], &wdir[j])) + show(o, Mflg, mstr, idx[i].path); + else + show(o, Tflg, tstr, idx[i].path); + i++; + j++; + /* only exists in index */ + }else if(c < 0){ + if(checkedin(&idx[i], 0)) + show(o, Rflg, rstr, idx[i].path); + else{ + idx[i].state = 'U'; + staleidx = 1; + } + i++; + /* only exists on disk */ + }else{ + if(checkedin(&wdir[j], 0)){ + if(samedata(nil, &wdir[j])) + show(o, Tflg, tstr, wdir[j].path); + else + show(o, Mflg, mstr, wdir[j].path); + }else if(printflg & Uflg && pfxmatch(wdir[j].path, argrel, argn, argc)) + show(o, Uflg, ustr, wdir[j].path); + j++; + } + } + Bterm(o); + + if(isindexed && staleidx) + if((wfd = create(".git/INDEX9.new", OWRITE, 0644)) != -1){ + if((w = Bfdopen(wfd, OWRITE)) == nil){ + close(wfd); + goto Nope; + } + for(i = 0; i < nidx; i++){ + while(i+1 < nidx && strcmp(idx[i].path, idx[i+1].path) == 0) + i++; + if(idx[i].state == 'U') + continue; + Bprint(w, "%c %Q %o %s\n", + idx[i].state, + idx[i].qid, + idx[i].mode, + idx[i].path); + } + Bterm(w); + nulldir(&rn); + rn.name = "INDEX9"; + if(remove(".git/INDEX9") == -1) + if(access(".git/INDEX9", AEXIST) == 0) + goto Nope; + if(dirwstat(".git/INDEX9.new", &rn) == -1) + sysfatal("rename: %r"); + } + +Nope: + if(!dirty) + exits(nil); + + p = xbuf; + e = p + sizeof(xbuf); + for(i = 0; (1 << i) != Tflg; i++) + if(dirty & (1 << i)) + p = seprint(p, e, "%c", "RMAUT"[i]); + *p = '\0'; + exits(xbuf); +}