├── .build.yml ├── LICENSE ├── README ├── add ├── branch ├── clone ├── commit ├── common.rc ├── compat ├── conf.c ├── delta.c ├── diff ├── export ├── extra ├── gitls └── gitrules ├── fetch.c ├── fs.c ├── get.c ├── git.1.man ├── git.h ├── gitfs.4.man ├── import ├── init ├── log ├── log.c ├── merge ├── mkfile ├── objset.c ├── ols.c ├── pack.c ├── proto.c ├── pull ├── push ├── query.c ├── rebase ├── ref.c ├── repack.c ├── revert ├── rm ├── save.c ├── send.c ├── serve.c ├── util.c └── walk.c /.build.yml: -------------------------------------------------------------------------------- 1 | image: 9front 2 | sources: 3 | - https://github.com/oridb/git9 4 | tasks: 5 | - build: | 6 | cd git9 7 | mk all 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Ori Bernstein 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Git for Plan 9: git/fs 2 | ====================== 3 | 4 | All commits in this repository were made with git9 running on 9front. 5 | 6 | About 7 | ----- 8 | 9 | Plan 9 is a non-posix system. Upstream git has been ported, but feels 10 | distinctly un-plan9ish, and even in its native environment, has been 11 | justifiably tarred and feathered for its user experience. 12 | 13 | Git/fs implements a git client for plan 9. The intent is to support 14 | working with git repositories, without cloning the git interface 15 | directly. 16 | 17 | Git/fs is my daily driver. It's solid, and works well for my needs. 18 | 19 | Getting Started 20 | --------------- 21 | 22 | Get a bootstrap copy: 23 | 24 | % cd /tmp 25 | % hget https://orib.dev/git/git9/HEAD/snap.tar.gz | tar xvz 26 | % cd git9 27 | % mk all 28 | % mk install 29 | 30 | Then get an updatable version from git: 31 | 32 | % cd $home/src 33 | % git/clone gits://orib.dev/git9 34 | % cd git9 35 | % mk all 36 | % mk install 37 | 38 | Configure your user name and email: 39 | 40 | % mkdir -p $home/lib/git 41 | % echo ' 42 | [user] 43 | name=Ori Bernstein 44 | email=ori@eigenstate.org 45 | ' > $home/lib/git/config 46 | 47 | Read the docs: 48 | 49 | % man 1 git 50 | % man 4 gitfs 51 | 52 | And start committing! 53 | 54 | Structure 55 | --------- 56 | 57 | The git/fs program provides a file system mounted on $repo/.git/fs. 58 | It provides a read-only view into the repository contents to allow 59 | scripts to inspect the data. Surrounding scripts and binaries will 60 | manipulate the repository contents directly. These changes will be 61 | immediately mirrored in the file system. 62 | 63 | Scripts will generally mount git/fs as needed to do 64 | their work, but if you want to browse the repository 65 | manually, run it yourself. You'll get `$repo/.git/fs` mounted, 66 | with the following contents: 67 | 68 | $repo/.git/fs/object: The objects in the repo. 69 | $repo/.git/fs/branch: The branches in the repo. 70 | $repo/.git/fs/ctl: A file showing the status of the repo. 71 | Currently, it only shows the current branch. 72 | $repo/.git/fs/HEAD An alias for the currently checked out 73 | commit directory. 74 | 75 | Visible Differences 76 | ------------------- 77 | 78 | The most obvious difference is that Git's index is a bit boneheaded, so I'm 79 | ignoring it. The index doesn't affect the wire protocol, so this 80 | isn't an interoperability issue, unless you share the same physical 81 | repository on both Plan 9 and Unix. If you do, expect them to disagree 82 | about the files that have been modified in the working copy. 83 | 84 | In fact, the entire concept of the staging area has been dropped, as 85 | it's both confusing and clunky. There are now only three states that 86 | files can be in: 'untracked', 'dirty', and 'committed'. Tracking is 87 | done with empty files under .git/index9/{removed,tracked}/path/to/file. 88 | 89 | It's implemented in Plan 9 flavor C, and provides tools for writing 90 | repository contents, and a file system for read-only access, which 91 | will mirror the current state of the repository. 92 | 93 | Installation 94 | ------------ 95 | 96 | Install with `mk install`. 97 | 98 | Examples 99 | -------- 100 | 101 | Some usage examples: 102 | 103 | git/clone git://git.eigenstate.org/ori/mc.git 104 | git/log 105 | cd subdir/name 106 | git/add foo.c 107 | diff bar.c $repo/.git/fs/HEAD/ 108 | git/commit foo.c 109 | git/push 110 | 111 | Commits are presented as directories with the following 112 | contents: 113 | 114 | author: A file containing the author name 115 | hash: A file containing the commit hash 116 | parent: A file containing the commit parents, one per line. 117 | msg: A file containing the log message for that commit 118 | tree: A directory containing a view of the repository. 119 | 120 | So, for example: 121 | 122 | % ls $repo/.git/fs/branch/heads/master 123 | $repo/.git/fs/branch/heads/master/author 124 | $repo/.git/fs/branch/heads/master/hash 125 | $repo/.git/fs/branch/heads/master/msg 126 | $repo/.git/fs/branch/heads/master/parent 127 | $repo/.git/fs/branch/heads/master/tree 128 | % cat $repo/.git/fs/branch/heads/master/hash 129 | 7d539a7c08aba3f31b3913e0efef11c43ea9 130 | 131 | # This is the same commit, with the same contents. 132 | % ls $repo/.git/fs/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef 133 | $repo/.git/fs/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef/author 134 | $repo/.git/fs/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef/hash 135 | $repo/.git/fs/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef/msg 136 | $repo/.git/fs/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef/parent 137 | $repo/.git/fs/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef/tree 138 | 139 | # what git/diff will hopefully do more concisely soon, filtering 140 | # out the non-git files. 141 | ape/diff -ur $repo/.git/fs/branch/heads/master/tree . 142 | Only in .: .git 143 | Only in .: debug 144 | diff -ur $repo/.git/fs/branch/heads/master/tree/fold.myr ./fold.myr 145 | --- $repo/.git/fs/branch/heads/master/tree/fold.myr Wed Dec 31 16:00:00 1969 146 | +++ ./fold.myr Mon Apr 1 21:39:06 2019 147 | @@ -6,6 +6,8 @@ 148 | const foldexpr : (e : expr# -> std.option(constval)) 149 | ;; 150 | 151 | +/* Look, diffing files just works, and I don't need any fancy glue! */ 152 | + 153 | const foldexpr = {e 154 | match e 155 | | &(`Eident &[.sc=`Sclassenum, .name=name, .ty=`Tyenum &(`Body enum)]): 156 | Only in .: refs 157 | 158 | 159 | The following utilities and binaries are provided: 160 | 161 | fs: The git filesystem. 162 | fetch: The protocol bits for getting data from a git server. 163 | send: The protocol bits for sending data to a git server. 164 | save: The gnarly bits for storing the files for a commit. 165 | conf: A program to extract information from a config file. 166 | clone: Clones a repository. 167 | commit: Commits a snapshot of the selected files. 168 | log: Prints the contents of a commmit log. 169 | add: Tells the repository to add a file to the next commit. 170 | walk: `du`, but for git status. 171 | ... and more 172 | 173 | Supported protocols: git:// and git+ssh://. If someone 174 | implements others, I'll gladly accept patches. 175 | 176 | 9legacy Notes 177 | ------------- 178 | 179 | To use git9 on 9legacy, these patches are necessary. 180 | 181 | - rc-line-split: implement delim{...} syntax for rc 182 | - walk: port walk command to 9legacy. 183 | - aux/getflags: 9front updated it with support for named args 184 | 185 | Additionally, git9 defaults to editing with hold(1). This command 186 | is not present on 9legacy. Import it, or set $editor to your choice 187 | of editor. 188 | -------------------------------------------------------------------------------- /add: -------------------------------------------------------------------------------- 1 | #!/bin/rc -e 2 | rfork ne 3 | . /sys/lib/git/common.rc 4 | 5 | gitup 6 | 7 | flagfmt='r:remove'; args='file ...' 8 | eval `''{aux/getflags $*} || exec aux/usage 9 | 10 | s=A 11 | if(~ $remove 1) 12 | s=R 13 | if(~ $#* 0) 14 | exec aux/usage 15 | 16 | paths=`$nl{cleanname -d $gitrel $* | drop $gitroot} 17 | walk -f ./$paths | grep -v '^(./)?.git/' | \ 18 | sed 's/^/'$s' NOQID 0 /' >> .git/INDEX9 19 | exit '' 20 | -------------------------------------------------------------------------------- /branch: -------------------------------------------------------------------------------- 1 | #!/bin/rc -e 2 | rfork en 3 | . /sys/lib/git/common.rc 4 | 5 | gitup 6 | 7 | flagfmt='a:listall, b:baseref ref, d:delete, n:newbr, s:stay, m:merge, M:nomod' 8 | args='[branch]' 9 | eval `''{aux/getflags $*} || exec aux/usage 10 | 11 | modified=() 12 | deleted=() 13 | 14 | if(~ $#* 0){ 15 | if(~ $#listall 0) 16 | awk '$1=="branch"{print $2}' < $gitfs/ctl 17 | if not 18 | cd .git/refs/ && walk -f heads remotes 19 | exit 20 | } 21 | if(! ~ $#* 1) 22 | exec aux/usage 23 | 24 | branch=$1 25 | if(~ $branch refs/heads/*) 26 | new=$name 27 | if not if(~ $branch heads/*) 28 | new=refs/$branch 29 | if not 30 | new=refs/heads/$branch 31 | 32 | orig=`{git/query HEAD} 33 | if (~ $#baseref 1) 34 | base=`{git/query $baseref} || exit 'bad base' 35 | if not if(~ $#newbr 0) 36 | base=`{git/query $new} 37 | if not 38 | base=`{git/query HEAD} 39 | 40 | if(~ $#newbr 0){ 41 | if(! ~ $#baseref 0) 42 | die update would clobber $branch with $baseref 43 | baseref=`$nl{echo -n $new | sed s@refs/heads/@refs/remotes/origin/@} 44 | if(! test -e .git/$new) 45 | if(! base=`{git/query $baseref}) 46 | die could not find branch $branch 47 | } 48 | modified=`$nl{git/query -c HEAD $base | grep '^[^-]' | subst '^..'} 49 | deleted=`$nl{git/query -c HEAD $base | grep '^-' | subst '^..'} 50 | 51 | # if we delete the current branch without switching, bad things happen 52 | if(~ $delete 1 && ~ `{git/query HEAD} `{git/query $branch}) 53 | die 'cannot delete current branch' 54 | # if we're not merging, don't clobber existing changes. 55 | if(~ $#merge 0 && ~ $#delete 0){ 56 | if(! ~ $#modified 0 || ! ~ $#deleted 0){ 57 | git/walk -fRMA $modified $deleted || 58 | die 'uncommitted changes would be clobbered' 59 | } 60 | } 61 | if(~ $delete 1){ 62 | rm -f .git/$new 63 | echo 'deleted branch' $new 64 | exit 65 | } 66 | commit=`{git/query $base} || die 'branch does not exist:' $base 67 | if(~ $new */*) 68 | mkdir -p .git/`{basename -d $new} 69 | if(! ~ $#stay 0){ 70 | echo $commit > .git/$new 71 | exit 72 | } 73 | basedir=`{git/query -p $base} 74 | dirtypaths=() 75 | if(! ~ $#modified 0 || ! ~ $#deleted 0) 76 | dirtypaths=`$nl{git/walk -cfRMA $modified $deleted} 77 | if(~ $#dirtypaths 0) 78 | cleanpaths=($modified $deleted) 79 | if not { 80 | cleanpaths=() 81 | for(p in $modified $deleted) 82 | if(! ~ $p $dirtypaths) 83 | cleanpaths=($cleanpaths $p) 84 | } 85 | 86 | echo $commit > .git/$new 87 | for(m in $cleanpaths){ 88 | d=`$nl{basename -d $m} 89 | mkdir -p $d 90 | # Modifications can turn a file into 91 | # a directory, or vice versa, so we 92 | # need to delete and copy the files 93 | # over. 94 | a=dir 95 | b=dir 96 | if(test -f $m) 97 | a=file 98 | if(test -f $basedir/tree/$m) 99 | b=file 100 | if(! ~ $a $b){ 101 | rm -rf $m 102 | echo R NOQID 0 $m >> .git/INDEX9 103 | } 104 | if(~ $b file){ 105 | cp -x -- $basedir/tree/$m $m 106 | echo T NOQID 0 $m >> .git/INDEX9 107 | touch $m 108 | } 109 | } 110 | 111 | for(ours in $dirtypaths){ 112 | common=$gitfs/object/$orig/tree/$ours 113 | theirs=$gitfs/object/$base/tree/$ours 114 | merge1 $ours $ours $common $theirs 115 | } 116 | 117 | for(d in $deleted){ 118 | if(! test -d $d){ 119 | rm -f $d 120 | echo R NOQID 0 $d >> .git/INDEX9 121 | } 122 | } 123 | 124 | echo ref: $new > .git/HEAD 125 | echo $new: `{git/query $new} 126 | exit '' 127 | -------------------------------------------------------------------------------- /clone: -------------------------------------------------------------------------------- 1 | #!/bin/rc 2 | rfork en 3 | . /sys/lib/git/common.rc 4 | 5 | flagfmt='d:debug, b:branch branch'; args='remote [local]' 6 | eval `''{aux/getflags $*} || exec aux/usage 7 | if(~ $debug 1) 8 | debug=(-d) 9 | 10 | remote=`{echo $1 | sed 's@/*$@@'} 11 | local=$2 12 | 13 | if(~ $#remote 0) 14 | exec aux/usage 15 | if(~ $#local 0) 16 | local=`{basename $remote .git} 17 | if(~ $#branch 1) 18 | branchflag=(-b $branch) 19 | 20 | if(test -e $local && ~ `{ls $local | sed 1q | wc -l} 1) 21 | die 'destination already exists:' $local 22 | 23 | fn clone{ 24 | flag +e 25 | mkdir -p $local/.git 26 | mkdir -p $local/.git/fs 27 | mkdir -p $local/.git/objects/pack/ 28 | mkdir -p $local/.git/refs/heads/ 29 | 30 | cd $local 31 | 32 | >>.git/config { 33 | echo '[remote "origin"]' 34 | echo ' url='$remote 35 | } 36 | {git/get $debug $branchflag $remote >[2=3] | awk ' 37 | BEGIN{ 38 | headref="" 39 | if(ENVIRON["branch"] != "") 40 | headref="refs/remotes/origin/"ENVIRON["branch"] 41 | headhash="" 42 | } 43 | /^symref / && headref == "" { 44 | if($2 == "HEAD"){ 45 | gsub("^refs/heads", "refs/remotes/origin", $3) 46 | gsub("^refs/tags", "refs/remotes/origin/tags", $3) 47 | } 48 | } 49 | /^remote /{ 50 | if($2=="HEAD"){ 51 | headhash=$3 52 | }else if(match($2, "^refs/(heads|tags)/")){ 53 | gsub("^refs/heads", "refs/remotes/origin", $2) 54 | if($2 == headref || (headref == "" && $3 == headhash)) 55 | headref=$2 56 | outfile = ".git/" $2 57 | outdir = outfile 58 | gsub("/?[^/]*/?$", "", outdir) 59 | system("mkdir -p "outdir) 60 | print $3 > outfile 61 | close(outfile) 62 | } 63 | } 64 | END{ 65 | if(headref != ""){ 66 | remote = headref; 67 | refdir = headref; 68 | gsub("/?[^/]*/?$", "", refdir) 69 | gsub("^refs/remotes/origin", "refs/heads", headref) 70 | system("mkdir -p `{basename -d .git/"headref"}"); 71 | system("cp .git/" remote " .git/" headref) 72 | print "ref: " headref > ".git/HEAD" 73 | }else if(headhash != ""){ 74 | print "warning: detached head "headhash > "/fd/2" 75 | print headhash > ".git/HEAD" 76 | } 77 | } 78 | '} |[3] tr '\x0d' '\x0a' || die 'could not clone repository' 79 | 80 | tree=.git/fs/HEAD/tree 81 | lbranch=`{git/branch} 82 | rbranch=`{echo $lbranch | subst 'heads' 'remotes/origin'} 83 | echo checking out repository... 84 | if(test -f .git/refs/$rbranch){ 85 | mkdir -p `{basename -d .git/refs/$lbranch} 86 | cp .git/refs/$rbranch .git/refs/$lbranch 87 | git/fs 88 | @ {builtin cd $tree && tar cif /fd/1 .} | @ {tar xf /fd/0} \ 89 | || die 'checkout failed:' $status 90 | {for(f in `$nl{cd $tree && walk -f}) 91 | echo 'T NOQID 0 '$f} > .git/INDEX9 92 | } 93 | if not{ 94 | echo no default branch >[1=2] 95 | echo check out your code with git/branch >[1=2] 96 | } 97 | } 98 | 99 | fn sigint { 100 | echo cancelled clone $remote: cleaning $local >[1=2] 101 | unmount $local/.git/fs >[2]/dev/null 102 | rm -rf $local 103 | exit interrupted 104 | } 105 | 106 | @{clone} 107 | st=$status 108 | if(! ~ $st ''){ 109 | echo failed to clone $remote: cleaning $local >[1=2] 110 | unmount $local/.git/fs >[2]/dev/null 111 | rm -rf $local 112 | exit $st 113 | } 114 | exit '' 115 | -------------------------------------------------------------------------------- /commit: -------------------------------------------------------------------------------- 1 | #!/bin/rc -e 2 | rfork ne 3 | . /sys/lib/git/common.rc 4 | 5 | fn findbranch{ 6 | branch=`{git/branch} 7 | if(test -e $gitfs/branch/$branch/tree){ 8 | refpath=.git/refs/$branch 9 | initial=false 10 | } 11 | if not if(test -e $gitfs/object/$branch/tree){ 12 | refpath=.git/HEAD 13 | initial=false 14 | } 15 | if not if(! test -e $gitfs/HEAD/tree){ 16 | refpath=.git/refs/$branch 17 | initial=true 18 | } 19 | if not 20 | die 'invalid branch:' $branch 21 | } 22 | 23 | # Remove commentary lines. 24 | # Remove leading and trailing empty lines. 25 | # Combine consecutive empty lines between paragraphs. 26 | # Remove trailing spaces from lines. 27 | # Ensure there's trailing newline. 28 | fn cleanmsg{ 29 | awk ' 30 | /^[ ]*#/ {next} 31 | /^[ ]*$/ {empty = 1; next} 32 | 33 | wet && empty {printf "\n"} 34 | {wet = 1; empty = 0} 35 | {sub(/[ ]+$/, ""); print $0} 36 | ' 37 | } 38 | 39 | fn editmsg{ 40 | if(! test -s $msgfile.tmp){ 41 | >$msgfile.tmp { 42 | echo '# Author:' $name '<'$email'>' 43 | echo '#' 44 | for(p in $parents) 45 | echo '# parent:' $p 46 | git/walk -fAMR $files | subst '^' '# ' 47 | echo '#' 48 | echo '# Commit message:' 49 | } 50 | edit=1 51 | } 52 | if(! ~ $#edit 0){ 53 | giteditor=`{git/conf core.editor} 54 | if(~ $#editor 0) 55 | editor=$giteditor 56 | if(~ $#editor 0) 57 | editor=hold 58 | $editor $msgfile.tmp 59 | } 60 | cleanmsg < $msgfile.tmp > $msgfile 61 | if(! test -s $msgfile) 62 | die 'empty commit message' 63 | } 64 | 65 | fn parents{ 66 | if(! ~ $#revise 0) 67 | parents=`{cat $gitfs/HEAD/parent} 68 | if not if(test -f .git/merge-parents) 69 | parents=`{cat .git/merge-parents | sort | uniq} 70 | if not if(~ $initial true) 71 | parents=() 72 | if not 73 | parents=`{git/query $branch} 74 | } 75 | 76 | fn commit{ 77 | msg=`''{cat $msgfile} 78 | if(! ~ $#parents 0) 79 | pflags='-p'^$parents 80 | hash=`{git/save -n $"name -e $"email -m $"msg $pflags $files || die $status} 81 | rm -f .git/merge-parents 82 | } 83 | 84 | fn update{ 85 | mkdir -p `{basename -d $refpath} 86 | # Paranoia: let's not mangle the repo. 87 | if(~ $#hash 0) 88 | die 'botched commit' 89 | echo $branch: $hash 90 | echo $hash > $refpath 91 | for(f in $files){ 92 | if(! test -e $f && ! test -e .git/object/$hash/tree/$f) 93 | echo R NOQID 0 $f >> .git/INDEX9 94 | if not 95 | echo T NOQID 0 $f >> .git/INDEX9 96 | } 97 | } 98 | 99 | fn sigexit{ 100 | if(! ~ $#msgfile 0) 101 | rm -f $msgfile $msgfile.tmp 102 | } 103 | 104 | gitup 105 | 106 | flagfmt='m:msg message, r:revise, e:edit'; args='[file ...]' 107 | eval `''{aux/getflags $*} || exec aux/usage 108 | 109 | msgfile=/tmp/git-msg.$pid 110 | if(~ $#msg 1) 111 | echo $msg >$msgfile.tmp 112 | if not if(~ $#revise 1){ 113 | msg=1 114 | echo revising commit `{cat $gitfs/HEAD/hash} 115 | cat $gitfs/HEAD/msg >$msgfile.tmp 116 | } 117 | 118 | files=() 119 | if(test -f .git/merge-parents) 120 | files=`$nl{git/query -c `{cat .git/merge-parents} | sed 's/^..//'} 121 | if(! ~ $#* 0) 122 | files=($files `$nl{git/walk -c `$nl{cleanname -d $gitrel $*}}) 123 | if(~ $status '' || ~ $#files 0 && ! test -f .git/merge-parents && ~ $#revise 0) 124 | die 'nothing to commit' 125 | @{ 126 | flag e + 127 | whoami 128 | findbranch 129 | parents 130 | editmsg 131 | commit 132 | update 133 | } || die 'could not commit:' $status 134 | exit '' 135 | -------------------------------------------------------------------------------- /common.rc: -------------------------------------------------------------------------------- 1 | nl=' 2 | ' 3 | 4 | fn die{ 5 | >[1=2] echo $0: $* 6 | exit $"* 7 | } 8 | 9 | fn usage{ 10 | >[1=2] echo -n 'usage:' $usage 11 | exit 'usage' 12 | } 13 | 14 | fn subst { 15 | awk ' 16 | BEGIN{ARGC=0} 17 | {sub(ARGV[1], ARGV[2]); print} 18 | ' $* 19 | } 20 | 21 | fn drop { 22 | awk ' 23 | BEGIN{ARGC=0} 24 | { 25 | if(index($0, ARGV[1]) == 1) 26 | $0=substr($0, length(ARGV[1])+1) 27 | print 28 | } 29 | ' $* 30 | } 31 | 32 | fn mergeperm { 33 | if(~ $1 /dev/null && cmp $2 $3>/dev/null) 34 | status=gone 35 | if not if (~ $3 /dev/null && cmp $1 $2>/dev/null) 36 | status=gone 37 | if not { 38 | mergedperms='-x' 39 | if(test -x $2){ 40 | if(test -x $1 -a -x $3) 41 | mergedperms='+x' 42 | } 43 | if not{ 44 | if(test -x $1 -o -x $3) 45 | mergedperms='+x' 46 | } 47 | status=() 48 | } 49 | } 50 | 51 | fn whoami{ 52 | name=`$nl{git/conf user.name} 53 | email=`$nl{git/conf user.email} 54 | if(test -f /adm/keys.who){ 55 | if(~ $name '') 56 | name=`$nl{awk -F'|' '$1=="'$user'" {x=$3} END{print x}' $tmp) 91 | echo merge needed: $out >[1=2] 92 | mv $tmp $out 93 | git/add $out 94 | chmod $mergedperms $out 95 | } 96 | if not { 97 | rm -f $tmp $out 98 | git/rm $out 99 | } 100 | }} 101 | 102 | fn gitup{ 103 | gitroot=`$nl{git/conf -r >[2]/dev/null} 104 | if(~ $#gitroot 0) 105 | die 'not a git repository' 106 | gitwork=`$nl{git/conf work.dir} 107 | if(~ $#gitwork 1) 108 | bind -c $gitwork/objects $gitroot/.git/objects 109 | if(~ $#workdir 1) 110 | bind $workdir .git/objects 111 | gitfs=$gitroot/.git/fs 112 | gitrel=`{pwd | drop $gitroot | sed 's@^/@@'} 113 | if(~ $#gitrel 0) 114 | gitrel='.' 115 | if(! cd $gitroot) 116 | die cd $gitroot: no repo there 117 | startfs=() 118 | mkdir -p $gitfs 119 | if(! test -e $gitfs/ctl) 120 | startfs=true 121 | if(! grep -s '^repo '$gitroot'$' $gitfs/ctl >[2]/dev/null) 122 | startfs=true 123 | if(~ $#startfs 1) 124 | git/fs 125 | if not 126 | status='' 127 | } 128 | -------------------------------------------------------------------------------- /compat: -------------------------------------------------------------------------------- 1 | #!/bin/rc 2 | 3 | rfork e 4 | 5 | opts=() 6 | args=() 7 | nl=' 8 | ' 9 | 10 | fn cmd_init{ 11 | while(! ~ $#* 0){ 12 | switch($1){ 13 | case --bare 14 | # ignore 15 | case -- 16 | # go likes to use these 17 | case -* 18 | die unknown command init $* 19 | case * 20 | args=($args $1) 21 | } 22 | shift 23 | } 24 | git/init $opts $args 25 | } 26 | 27 | fn cmd_clone{ 28 | branch=() 29 | while( ! ~ $#* 0){ 30 | switch($1){ 31 | case -b 32 | branch=$2 33 | shift 34 | case -- 35 | # go likes to use these 36 | case -* 37 | die unknown command clone $* 38 | case * 39 | args=($args $1) 40 | } 41 | shift 42 | } 43 | git/clone $opts $args 44 | if(~ $#branch 1) 45 | git/branch -n -b $1 origin/$1 46 | } 47 | 48 | fn cmd_pull{ 49 | if(~ $1 -*) 50 | die unknown options for pull $* 51 | git/pull 52 | } 53 | 54 | fn cmd_fetch{ 55 | while(~ $#* 0){ 56 | switch($1){ 57 | case --all 58 | opts=($opts -a) 59 | case -f 60 | opts=($opts -u $2) 61 | shift 62 | case -- 63 | # go likes to use these 64 | case -* 65 | die unknown command clone $* 66 | case * 67 | args=($args $1) 68 | } 69 | shift 70 | } 71 | git/pull -f $opts 72 | } 73 | 74 | 75 | fn cmd_checkout{ 76 | if(~ $1 -*) 77 | die unknown command pull $* 78 | if(~ $#* 0) 79 | die git checkout branch 80 | git/branch $b 81 | } 82 | 83 | fn cmd_submodule { 84 | if(test -f .gitmodules) 85 | die 'submodules unsupported' 86 | } 87 | 88 | fn cmd_rev-parse{ 89 | while(~ $1 -*){ 90 | switch($1){ 91 | case --git-dir 92 | echo $gitroot/.git 93 | shift 94 | case --abbrev-ref 95 | echo `{dcmd git9/branch | sed s@^heads/@@g} 96 | shift 97 | case * 98 | die unknown option $opt 99 | } 100 | shift 101 | } 102 | } 103 | 104 | fn cmd_show-ref{ 105 | if(~ $1 -*) 106 | die unknown command pull $* 107 | filter=cat 108 | if(~ $#* 0) 109 | filter=cat 110 | if not 111 | filter='-e(^|/)'^$*^'$' 112 | for(b in `$nl{cd $gitroot/.git/refs/ && walk -f}) 113 | echo `{cat $gitroot/.git/refs/$b} refs/$b 114 | } 115 | 116 | fn cmd_rev-parse{ 117 | switch($1){ 118 | case --git-dir 119 | echo `{git/conf -r}^/.git 120 | case * 121 | die 'unknown rev-parse '$* 122 | } 123 | } 124 | 125 | fn cmd_remote{ 126 | if({! ~ $#* 3 && ! ~ $#* 4} || ! ~ $1 add) 127 | die unimplemented remote cmd $* 128 | name=$2 129 | url=$3 130 | if(~ $3 '--') 131 | url=$4 132 | >>$gitroot/.git/config{ 133 | echo '[remote "'$name'"]' 134 | echo ' url='$url 135 | } 136 | } 137 | 138 | fn cmd_log{ 139 | count=() 140 | format='' 141 | while(~ $1 -*){ 142 | switch($1){ 143 | case --format 144 | format=$2 145 | shift 146 | case '--format='* 147 | format=`{echo $1 | sed 's/--format=//g'} 148 | case -n 149 | count=-n$2 150 | shift 151 | case -n* 152 | count=$1 153 | case * 154 | dprint option $opt 155 | } 156 | shift 157 | } 158 | @{cd $gitroot && git/fs} 159 | switch($format){ 160 | case '' 161 | git/log $count 162 | case '%H:%ct' 163 | for(c in `{git/log -s $count| awk '{print $1}'}) 164 | echo $c:`{mtime $gitroot/.git/fs/object/$c/msg} 165 | case '%h %cd' 166 | for(c in `{git/log -s $count| awk '{print $1}'}) 167 | echo $c `{date `{mtime $gitroot/.git/fs/object/$c/msg}} 168 | } 169 | 170 | } 171 | 172 | fn cmd_show{ 173 | cmd_log -n1 $* 174 | } 175 | 176 | fn cmd_ls-remote{ 177 | if(~ $1 -q) 178 | shift 179 | remote=`$nl{git/conf 'remote "'$1'".url'} 180 | if(~ $#remote 0) 181 | remote=$1 182 | git/get -l $remote | awk '/^remote/{print $3"\t"$2}' 183 | } 184 | 185 | fn cmd_version{ 186 | echo git version 2.2.0 187 | } 188 | 189 | fn cmd_status{ 190 | echo 191 | } 192 | 193 | fn usage{ 194 | echo 'git ' >[1=2] 195 | exit usage 196 | } 197 | 198 | fn die { 199 | >[1=2] echo git $_cmdname: $* 200 | exit $_cmdname: $* 201 | } 202 | 203 | _cmdname=$1 204 | if(~ $0 *compat){ 205 | ramfs -m /n/gitcompat 206 | touch /n/gitcompat/git 207 | bind $0 /n/gitcompat/git 208 | path=( /n/gitcompat $path ) 209 | exec rc 210 | } 211 | 212 | if(~ $#gitcompatdebug 1) 213 | echo running $* >>/tmp/gitlog 214 | 215 | if(~ $1 -c) 216 | shift 2 217 | if(~ $1 -c*) 218 | shift 1 219 | if(! test -f '/env/fn#cmd_'$1) 220 | die git $1: commmand not implemented 221 | if(! ~ $1 init && ! ~ $1 clone) 222 | gitroot=`{git/conf -r} || die repo 223 | 224 | if(~ $#gitcompatdebug 1) 225 | cmd_$1 $*(2-) | tee >>/tmp/gitlog 226 | if not 227 | cmd_$1 $*(2-) 228 | exit '' 229 | -------------------------------------------------------------------------------- /conf.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "git.h" 6 | 7 | int findroot; 8 | int showall; 9 | int nfile; 10 | char *file[32]; 11 | 12 | static int 13 | showconf(char *cfg, char *sect, char *key) 14 | { 15 | char *ln, *p; 16 | Biobuf *f; 17 | int foundsect, nsect, nkey, found; 18 | 19 | if((f = Bopen(cfg, OREAD)) == nil) 20 | return 0; 21 | 22 | found = 0; 23 | nsect = sect ? strlen(sect) : 0; 24 | nkey = strlen(key); 25 | foundsect = (sect == nil); 26 | while((ln = Brdstr(f, '\n', 1)) != nil){ 27 | p = strip(ln); 28 | if(*p == '[' && sect){ 29 | foundsect = strncmp(sect, ln, nsect) == 0; 30 | }else if(foundsect && strncmp(p, key, nkey) == 0){ 31 | p = strip(p + nkey); 32 | if(*p != '=') 33 | continue; 34 | p = strip(p + 1); 35 | print("%s\n", p); 36 | found = 1; 37 | if(!showall){ 38 | free(ln); 39 | goto done; 40 | } 41 | } 42 | free(ln); 43 | } 44 | done: 45 | return found; 46 | } 47 | 48 | 49 | void 50 | usage(void) 51 | { 52 | fprint(2, "usage: %s [-f file] [-r] keys..\n", argv0); 53 | fprint(2, "\t-f: use file 'file' (default: .git/config)\n"); 54 | fprint(2, "\t r: print repository root\n"); 55 | exits("usage"); 56 | } 57 | 58 | void 59 | main(int argc, char **argv) 60 | { 61 | char repo[512], *p, *s; 62 | int i, j, nrel; 63 | 64 | ARGBEGIN{ 65 | case 'f': file[nfile++]=EARGF(usage()); break; 66 | case 'r': findroot++; break; 67 | case 'a': showall++; break; 68 | default: usage(); break; 69 | }ARGEND; 70 | 71 | if(findroot){ 72 | if(findrepo(repo, sizeof(repo), &nrel) == -1) 73 | sysfatal("%r"); 74 | print("%s\n", repo); 75 | exits(nil); 76 | } 77 | if(nfile == 0){ 78 | file[nfile++] = ".git/config"; 79 | if((p = getenv("home")) != nil) 80 | file[nfile++] = smprint("%s/lib/git/config", p); 81 | file[nfile++] = "/sys/lib/git/config"; 82 | } 83 | 84 | for(i = 0; i < argc; i++){ 85 | if((p = strchr(argv[i], '.')) == nil){ 86 | s = nil; 87 | p = argv[i]; 88 | }else{ 89 | *p = 0; 90 | p++; 91 | s = smprint("[%s]", argv[i]); 92 | } 93 | for(j = 0; j < nfile; j++) 94 | if(showconf(file[j], s, p)) 95 | break; 96 | } 97 | exits(nil); 98 | } 99 | -------------------------------------------------------------------------------- /delta.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "git.h" 5 | 6 | enum { 7 | Minchunk = 128, 8 | Maxchunk = 8192, 9 | Splitmask = (1<<8)-1, 10 | }; 11 | 12 | static u32int geartab[] = { 13 | 0x67ed26b7, 0x32da500c, 0x53d0fee0, 0xce387dc7, 0xcd406d90, 0x2e83a4d4, 0x9fc9a38d, 0xb67259dc, 14 | 0xca6b1722, 0x6d2ea08c, 0x235cea2e, 0x3149bb5f, 0x1beda787, 0x2a6b77d5, 0x2f22d9ac, 0x91fc0544, 15 | 0xe413acfa, 0x5a30ff7a, 0xad6fdde0, 0x444fd0f5, 0x7ad87864, 0x58c5ff05, 0x8d2ec336, 0x2371f853, 16 | 0x550f8572, 0x6aa448dd, 0x7c9ddbcf, 0x95221e14, 0x2a82ec33, 0xcbec5a78, 0xc6795a0d, 0x243995b7, 17 | 0x1c909a2f, 0x4fded51c, 0x635d334b, 0x0e2b9999, 0x2702968d, 0x856de1d5, 0x3325d60e, 0xeb6a7502, 18 | 0xec2a9844, 0x0905835a, 0xa1820375, 0xa4be5cab, 0x96a6c058, 0x2c2ccd70, 0xba40fce3, 0xd794c46b, 19 | 0x8fbae83e, 0xc3aa7899, 0x3d3ff8ed, 0xa0d42b5b, 0x571c0c97, 0xd2811516, 0xf7e7b96c, 0x4fd2fcbd, 20 | 0xe2fdec94, 0x282cc436, 0x78e8e95c, 0x80a3b613, 0xcfbee20c, 0xd4a32d1c, 0x2a12ff13, 0x6af82936, 21 | 0xe5630258, 0x8efa6a98, 0x294fb2d1, 0xdeb57086, 0x5f0fddb3, 0xeceda7ce, 0x4c87305f, 0x3a6d3307, 22 | 0xe22d2942, 0x9d060217, 0x1e42ed02, 0xb6f63b52, 0x4367f39f, 0x055cf262, 0x03a461b2, 0x5ef9e382, 23 | 0x386bc03a, 0x2a1e79c7, 0xf1a0058b, 0xd4d2dea9, 0x56baf37d, 0x5daff6cc, 0xf03a951d, 0xaef7de45, 24 | 0xa8f4581e, 0x3960b555, 0xffbfff6d, 0xbe702a23, 0x8f5b6d6f, 0x061739fb, 0x98696f47, 0x3fd596d4, 25 | 0x151eac6b, 0xa9fcc4f5, 0x69181a12, 0x3ac5a107, 0xb5198fe7, 0x96bcb1da, 0x1b5ddf8e, 0xc757d650, 26 | 0x65865c3a, 0x8fc0a41a, 0x87435536, 0x99eda6f2, 0x41874794, 0x29cff4e8, 0xb70efd9a, 0x3103f6e7, 27 | 0x84d2453b, 0x15a450bd, 0x74f49af1, 0x60f664b1, 0xa1c86935, 0xfdafbce1, 0xe36353e3, 0x5d9ba739, 28 | 0xbc0559ba, 0x708b0054, 0xd41d808c, 0xb2f31723, 0x9027c41f, 0xf136d165, 0xb5374b12, 0x9420a6ac, 29 | 0x273958b6, 0xe6c2fad0, 0xebdc1f21, 0xfb33af8b, 0xc71c25cd, 0xe9a2d8e5, 0xbeb38a50, 0xbceb7cc2, 30 | 0x4e4e73f0, 0xcd6c251d, 0xde4c032c, 0x4b04ac30, 0x725b8b21, 0x4eb8c33b, 0x20d07b75, 0x0567aa63, 31 | 0xb56b2bb7, 0xc1f5fd3a, 0xcafd35ca, 0x470dd4da, 0xfe4f94cd, 0xfb8de424, 0xe8dbcf40, 0xfe50a37a, 32 | 0x62db5b5d, 0xf32f4ab6, 0x2c4a8a51, 0x18473dc0, 0xfe0cbb6e, 0xfe399efd, 0xdf34ecc9, 0x6ccd5055, 33 | 0x46097073, 0x139135c2, 0x721c76f6, 0x1c6a94b4, 0x6eee014d, 0x8a508e02, 0x3da538f5, 0x280d394f, 34 | 0x5248a0c4, 0x3ce94c6c, 0x9a71ad3a, 0x8493dd05, 0xe43f0ab6, 0x18e4ed42, 0x6c5c0e09, 0x42b06ec9, 35 | 0x8d330343, 0xa45b6f59, 0x2a573c0c, 0xd7fd3de6, 0xeedeab68, 0x5c84dafc, 0xbbd1b1a8, 0xa3ce1ad1, 36 | 0x85b70bed, 0xb6add07f, 0xa531309c, 0x8f8ab852, 0x564de332, 0xeac9ed0c, 0x73da402c, 0x3ec52761, 37 | 0x43af2f4d, 0xd6ff45c8, 0x4c367462, 0xd553bd6a, 0x44724855, 0x3b2aa728, 0x56e5eb65, 0xeaf16173, 38 | 0x33fa42ff, 0xd714bb5d, 0xfbd0a3b9, 0xaf517134, 0x9416c8cd, 0x534cf94f, 0x548947c2, 0x34193569, 39 | 0x32f4389a, 0xfe7028bc, 0xed73b1ed, 0x9db95770, 0x468e3922, 0x0440c3cd, 0x60059a62, 0x33504562, 40 | 0x2b229fbd, 0x5174dca5, 0xf7028752, 0xd63c6aa8, 0x31276f38, 0x0646721c, 0xb0191da8, 0xe00e6de0, 41 | 0x9eac1a6e, 0x9f7628a5, 0xed6c06ea, 0x0bb8af15, 0xf119fb12, 0x38693c1c, 0x732bc0fe, 0x84953275, 42 | 0xb82ec888, 0x33a4f1b3, 0x3099835e, 0x028a8782, 0x5fdd51d7, 0xc6c717b3, 0xb06caf71, 0x17c8c111, 43 | 0x61bad754, 0x9fd03061, 0xe09df1af, 0x3bc9eb73, 0x85878413, 0x9889aaf2, 0x3f5a9e46, 0x42c9f01f, 44 | 0x9984a4f4, 0xd5de43cc, 0xd294daed, 0xbecba2d2, 0xf1f6e72c, 0x5551128a, 0x83af87e2, 0x6f0342ba, 45 | }; 46 | 47 | static u64int 48 | hash(void *p, int n) 49 | { 50 | return murmurhash2(p, n); 51 | } 52 | 53 | static void 54 | addblk(Dtab *dt, void *buf, int len, int off, u64int h) 55 | { 56 | int i, sz, probe; 57 | Dblock *db; 58 | 59 | probe = h % dt->sz; 60 | while(dt->b[probe].buf != nil){ 61 | if(len == dt->b[probe].len && memcmp(buf, dt->b[probe].buf, len) == 0) 62 | return; 63 | probe = (probe + 1) % dt->sz; 64 | } 65 | assert(dt->b[probe].buf == nil); 66 | dt->b[probe].buf = buf; 67 | dt->b[probe].len = len; 68 | dt->b[probe].off = off; 69 | dt->b[probe].hash = h; 70 | dt->nb++; 71 | if(dt->sz < 2*dt->nb){ 72 | sz = dt->sz; 73 | db = dt->b; 74 | dt->sz *= 2; 75 | dt->nb = 0; 76 | dt->b = eamalloc(dt->sz, sizeof(Dblock)); 77 | for(i = 0; i < sz; i++) 78 | if(db[i].buf != nil) 79 | addblk(dt, db[i].buf, db[i].len, db[i].off, db[i].hash); 80 | free(db); 81 | } 82 | } 83 | 84 | static Dblock* 85 | lookup(Dtab *dt, uchar *p, int n) 86 | { 87 | int probe; 88 | u64int h; 89 | 90 | h = hash(p, n); 91 | for(probe = h % dt->sz; dt->b[probe].buf != nil; probe = (probe + 1) % dt->sz){ 92 | if(dt->b[probe].hash != h) 93 | continue; 94 | if(n != dt->b[probe].len) 95 | continue; 96 | if(memcmp(p, dt->b[probe].buf, n) != 0) 97 | continue; 98 | return &dt->b[probe]; 99 | } 100 | return nil; 101 | } 102 | 103 | static int 104 | nextblk(uchar *s, uchar *e) 105 | { 106 | u32int gh; 107 | uchar *p; 108 | 109 | if((e - s) < Minchunk) 110 | return e - s; 111 | p = s + Minchunk; 112 | if((e - s) > Maxchunk) 113 | e = s + Maxchunk; 114 | gh = 0; 115 | while(p != e){ 116 | gh = (gh<<1) + geartab[*p++]; 117 | if((gh & Splitmask) == 0) 118 | break; 119 | } 120 | return p - s; 121 | } 122 | 123 | void 124 | dtinit(Dtab *dt, Object *obj) 125 | { 126 | uchar *s, *e; 127 | u64int h; 128 | vlong n, o; 129 | 130 | o = 0; 131 | s = (uchar*)obj->data; 132 | e = s + obj->size; 133 | dt->o = ref(obj); 134 | dt->nb = 0; 135 | dt->sz = 128; 136 | dt->b = eamalloc(dt->sz, sizeof(Dblock)); 137 | dt->base = (uchar*)obj->data; 138 | dt->nbase = obj->size; 139 | while(s != e){ 140 | n = nextblk(s, e); 141 | h = hash(s, n); 142 | addblk(dt, s, n, o, h); 143 | s += n; 144 | o += n; 145 | } 146 | } 147 | 148 | void 149 | dtclear(Dtab *dt) 150 | { 151 | unref(dt->o); 152 | free(dt->b); 153 | } 154 | 155 | static int 156 | emitdelta(Delta **pd, int *nd, int cpy, int off, int len) 157 | { 158 | Delta *d; 159 | 160 | *nd += 1; 161 | *pd = earealloc(*pd, *nd, sizeof(Delta)); 162 | d = &(*pd)[*nd - 1]; 163 | d->cpy = cpy; 164 | d->off = off; 165 | d->len = len; 166 | return len; 167 | } 168 | 169 | static int 170 | stretch(Dtab *dt, Dblock *b, uchar *s, uchar *e, int n) 171 | { 172 | uchar *p0, *p, *q, *eb; 173 | 174 | if(b == nil) 175 | return n; 176 | p = s + n; 177 | q = dt->base + b->off + n; 178 | p0 = p; 179 | if(dt->nbase < (1<<24)-1) 180 | eb = dt->base + dt->nbase; 181 | else 182 | eb = dt->base + (1<<24)-1; 183 | while(1){ 184 | if(p == e || q == eb) 185 | break; 186 | if(*p != *q) 187 | break; 188 | p++; 189 | q++; 190 | } 191 | return n + (p - p0); 192 | } 193 | 194 | Delta* 195 | deltify(Object *obj, Dtab *dt, int *pnd) 196 | { 197 | Delta *d; 198 | Dblock *b; 199 | uchar *s, *e; 200 | vlong n, o; 201 | 202 | o = 0; 203 | d = nil; 204 | s = (uchar*)obj->data; 205 | e = s + obj->size; 206 | *pnd = 0; 207 | while(s != e){ 208 | n = nextblk(s, e); 209 | b = lookup(dt, s, n); 210 | n = stretch(dt, b, s, e, n); 211 | if(b != nil) 212 | emitdelta(&d, pnd, 1, b->off, n); 213 | else 214 | emitdelta(&d, pnd, 0, o, n); 215 | s += n; 216 | o += n; 217 | } 218 | return d; 219 | } 220 | -------------------------------------------------------------------------------- /diff: -------------------------------------------------------------------------------- 1 | #!/bin/rc 2 | rfork ne 3 | . /sys/lib/git/common.rc 4 | 5 | gitup 6 | 7 | flagfmt='c:commit branch, s:summarize, u:uncommitted'; args='[file ...]' 8 | eval `''{aux/getflags $*} || exec aux/usage 9 | 10 | if(~ $#commit 0){ 11 | commit=HEAD 12 | cparam=() 13 | } 14 | 15 | files=() 16 | filt=MAR 17 | if(~ $#uncommitted 1) 18 | filt=MARU 19 | if(! ~ $#* 0) 20 | files=`{cleanname -d $gitrel $*} 21 | 22 | branch=`{git/query -p $commit} 23 | if(~ $summarize 1 || ~ $uncommitted 1){ 24 | git/walk -f$filt $cparam $files 25 | exit 26 | } 27 | 28 | showed=() 29 | mntgen /mnt/scratch 30 | bind $branch/tree/ /mnt/scratch/a 31 | bind . /mnt/scratch/b 32 | for(f in `$nl{git/walk -c -f$filt $cparam $files}){ 33 | if(~ $#showed 0){ 34 | echo diff `{git/query $commit} uncommitted 35 | showed=1 36 | } 37 | cd /mnt/scratch 38 | a=a/$f 39 | b=b/$f 40 | if(! test -f a/$f) 41 | a=/dev/null 42 | if(! test -f b/$f) 43 | b=/dev/null 44 | diff -u $a $b 45 | } 46 | exit '' 47 | -------------------------------------------------------------------------------- /export: -------------------------------------------------------------------------------- 1 | #!/bin/rc 2 | rfork ne 3 | . /sys/lib/git/common.rc 4 | 5 | patchname=/tmp/git.patchname.$pid 6 | patchfile=/tmp/git.patchfile.$pid 7 | fn sigexit{ 8 | rm -f $patchname $patchfile 9 | } 10 | 11 | gitup 12 | 13 | flagfmt='o:patchdir patchdir'; args='[query]' 14 | eval `''{aux/getflags $*} || exec aux/usage 15 | 16 | if(~ $#patchdir 1 && ! test -d $patchdir) 17 | mkdir -p $patchdir 18 | 19 | q=$* 20 | if(~ $#q 0) 21 | q=HEAD 22 | commits=`{git/query $q || die $status} 23 | n=1 24 | m=$#commits 25 | 26 | 27 | # sleazy hack: we want to run 28 | # under rfork m for the web ui, 29 | # so don't error if we can't mount 30 | mntgen /mnt/scratch >[2]/dev/null || status='' 31 | for(c in $commits){ 32 | cp=`{git/query -p $c} 33 | pp=`{git/query -p $c'~'} 34 | fc=`$nl{git/query -c $c~ $c | sed 's/^..//'} 35 | 36 | @{ 37 | rfork n 38 | cd /mnt/scratch 39 | if(test -d $pp/tree) 40 | bind $pp/tree a 41 | if(test -d $cp/tree) 42 | bind $cp/tree b 43 | 44 | echo From: `{cat $cp/author} 45 | echo Date: `{date -uf'WW, DD MMM YYYY hh:mm:ss Z' `{walk -em $cp/author}} 46 | <$cp/msg awk ' 47 | NR == 1 { 48 | n = ENVIRON["n"] 49 | m = ENVIRON["m"] 50 | msg=$0 51 | if(m > 1) 52 | patch = sprintf("[PATCH %d/%d]", n, m) 53 | else 54 | patch = "[PATCH]" 55 | printf "Subject: %s %s\n\n", patch, msg 56 | 57 | gsub("^[ ]|[ ]$", "", msg) 58 | gsub("[^a-zA-Z0-9_]+", "-", msg) 59 | printf "%.4d-%s.patch", n, msg >ENVIRON["patchname"] 60 | next 61 | } 62 | { 63 | print 64 | }' 65 | echo '---' 66 | echo diff `{basename $pp} `{basename $cp} 67 | for(f in $fc){ 68 | a=a/$f 69 | if(! test -e $a) 70 | a=/dev/null 71 | b=b/$f 72 | if(! test -e $b) 73 | b=/dev/null 74 | diff -ur $a $b 75 | } 76 | } >$patchfile 77 | if(~ $#patchdir 0){ 78 | cat $patchfile 79 | ! ~ $n $m && echo 80 | } 81 | if not{ 82 | f=$patchdir/`{cat $patchname} 83 | mv $patchfile $f 84 | echo $f 85 | } 86 | n=`{echo $n + 1 | bc} 87 | } 88 | exit '' 89 | -------------------------------------------------------------------------------- /extra/gitls: -------------------------------------------------------------------------------- 1 | #!/bin/rc -e 2 | 3 | cd $1 4 | shift 5 | 6 | rfork ne 7 | nl=' 8 | ' 9 | 10 | fn htcat { 11 | sed ' 12 | s/&/\&/g; 13 | s//\>/g; 15 | s/"/\"/g; 16 | s/''/\'/g 17 | ' $* 18 | } 19 | 20 | fn resolveref { 21 | if(~ $refname HEAD) 22 | echo $refname 23 | if not if(test -d $gitfs/branch/$refname/tree) 24 | echo branch/$refname 25 | if not if(test -d $gitfs/object/$refname/tree) 26 | echo object/$refname 27 | if not 28 | status='bad ref' 29 | } 30 | 31 | fn repons { 32 | mntgen 33 | mntgen /mnt/mnt 34 | bind /bin /mnt/bin 35 | bind /tmp /mnt/tmp 36 | bind -c /env /mnt/env 37 | bind $1 /mnt/$repo 38 | bind /mnt / 39 | cd /mnt/$repo 40 | git/fs 41 | rfork m 42 | } 43 | 44 | 45 | fn prelude { 46 | echo ' 47 | 48 | 49 | 50 | 51 | 72 | 73 | 74 | git webls 75 | 76 | 77 | ' 78 | } 79 | 80 | switch($1){ 81 | case 'tar' 82 | repo=$2 83 | refname=$3 84 | @{ 85 | repons $2 86 | if(! ref=`{resolveref $refname}){ 87 | echo 'invalid ref '$refname'' 88 | exit 89 | } 90 | bind $gitfs/$ref/tree /mnt/$repo 91 | cd /mnt 92 | tar cz $repo 93 | } 94 | 95 | case 'list' 96 | rfork m 97 | prelude 98 | echo '

Repos

99 |
' 100 | for(repo in `$nl{ls}){ 101 | if(test -e $repo/.git/webpublish){ 102 | echo '
'$repo'
' 103 | echo '
' 104 | if(test -f $repo/.git/desc) 105 | htcat $repo/.git/desc 106 | if not 107 | echo 'code some guy wrote' 108 | echo '
' 109 | } 110 | } 111 | echo '
' 112 | 113 | case 'info' 114 | repo=$2 115 | repodir=/mnt/$repo/.git 116 | refname=$3 117 | @{ 118 | repons $repo 119 | if(! ref=`{resolveref $refname}){ 120 | echo 'invalid ref '$refname'' 121 | exit 122 | } 123 | cd $gitfs/$ref/tree 124 | hash=`{cat $gitfs/$ref/hash} 125 | 126 | prelude $repo $ref $repo 127 | echo '

Git: '$repo'

129 |

'$repo' @ '$hash' 130 |

'
131 | 	htcat $gitfs/object/$hash/msg
132 | 	echo '	
133 |

Code

134 |

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

141 |
'
142 | 	for(f in `$nl{ls}){
143 | 		url=`$nl{echo -n $f/f.html | urlencode}
144 | 		fname=`$nl{echo -n $f | htcat}
145 | 		echo ''$fname''
146 | 	}		
147 | 	echo '
148 |

About This Repo

149 |
'
150 | 	if(test -f $repodir/README)
151 | 		htcat $repodir/README
152 | 	if not if(test -f README)
153 | 		htcat README
154 | 	if not if (test -f README.md)
155 | 		htcat README.md
156 | 	if not if(test -f $repodir/desc)
157 | 		htcat $repodir/desc
158 | 	if not
159 | 		echo 'this repo has no description'
160 | 	echo '
161 | 		
162 | 163 | 164 | ' 165 | } 166 | 167 | case 'view' 168 | repo=$2 169 | repodir=/mnt/$repo/.git 170 | refname=$3 171 | file=$4 172 | @{ 173 | repons $repo 174 | if(! ref=`{resolveref $refname}){ 175 | echo 'invalid ref '$refname'' 176 | exit 177 | } 178 | cd $gitfs/$ref/tree 179 | if(~ $file '') 180 | file='.' 181 | hash=`{cat $gitfs/$ref/hash} 182 | 183 | prelude 184 | echo '

Git: '$repo'

186 |

'$repo' @ '$hash' 187 |

'
188 | 	if(test -f $file){
189 | 		htcat $file
190 | 	}
191 | 	if not if(test -d $file){
192 | 		cd $file
193 | 		for(f in `$nl{ls}){
194 | 			url=`$nl{echo -n $f/f.html | urlencode}
195 | 			fname=`$nl{echo -n $f | htcat}
196 | 			echo ''$fname''
197 | 		}
198 | 	}
199 | 	echo '	
200 | 201 | ' 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /extra/gitrules: -------------------------------------------------------------------------------- 1 | /repos.html /bin/gitls /usr/git list 2 | /([^'/]+)/([^'/]+)/info.html /bin/gitls /usr/git info '\1' '\2' 3 | /([^'/]+)/([^'/]+)/snap.tar.gz /bin/gitls /usr/git tar '\1' '\2' 4 | /([^'/]+)/([^'/]+)/(([^']+)/)?f.html /bin/gitls /usr/git view '\1' '\2' '\4' 5 | -------------------------------------------------------------------------------- /fetch.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "git.h" 5 | 6 | char *fetchbranch; 7 | char *upstream = "origin"; 8 | char *packtmp = ".git/objects/pack/fetch.tmp"; 9 | int listonly; 10 | 11 | int 12 | resolveremote(Hash *h, char *ref) 13 | { 14 | char buf[128], *s; 15 | int r, f; 16 | 17 | ref = strip(ref); 18 | if((r = hparse(h, ref)) != -1) 19 | return r; 20 | /* Slightly special handling: translate remote refs to local ones. */ 21 | if(strcmp(ref, "HEAD") == 0){ 22 | snprint(buf, sizeof(buf), ".git/HEAD"); 23 | }else if(strstr(ref, "refs/heads") == ref){ 24 | ref += strlen("refs/heads"); 25 | snprint(buf, sizeof(buf), ".git/refs/remotes/%s/%s", upstream, ref); 26 | }else if(strstr(ref, "refs/tags") == ref){ 27 | ref += strlen("refs/tags"); 28 | snprint(buf, sizeof(buf), ".git/refs/tags/%s/%s", upstream, ref); 29 | }else{ 30 | return -1; 31 | } 32 | 33 | r = -1; 34 | s = strip(buf); 35 | if((f = open(s, OREAD)) == -1) 36 | return -1; 37 | if(readn(f, buf, sizeof(buf)) >= 40) 38 | r = hparse(h, buf); 39 | close(f); 40 | 41 | if(r == -1 && strstr(buf, "ref:") == buf) 42 | return resolveremote(h, buf + strlen("ref:")); 43 | return r; 44 | } 45 | 46 | int 47 | rename(char *pack, char *idx, Hash h) 48 | { 49 | char name[128]; 50 | Dir st; 51 | 52 | nulldir(&st); 53 | st.name = name; 54 | snprint(name, sizeof(name), "%H.pack", h); 55 | if(access(name, AEXIST) == 0) 56 | fprint(2, "warning, pack %s already fetched\n", name); 57 | else if(dirwstat(pack, &st) == -1) 58 | return -1; 59 | snprint(name, sizeof(name), "%H.idx", h); 60 | if(access(name, AEXIST) == 0) 61 | fprint(2, "warning, pack %s already indexed\n", name); 62 | else if(dirwstat(idx, &st) == -1) 63 | return -1; 64 | return 0; 65 | } 66 | 67 | int 68 | checkhash(int fd, vlong sz, Hash *hcomp) 69 | { 70 | DigestState *st; 71 | Hash hexpect; 72 | char buf[Pktmax]; 73 | vlong n, r; 74 | int nr; 75 | 76 | if(sz < 28){ 77 | werrstr("undersize packfile"); 78 | return -1; 79 | } 80 | 81 | st = nil; 82 | n = 0; 83 | while(n != sz - 20){ 84 | nr = sizeof(buf); 85 | if(sz - n - 20 < sizeof(buf)) 86 | nr = sz - n - 20; 87 | r = readn(fd, buf, nr); 88 | if(r != nr) 89 | return -1; 90 | st = sha1((uchar*)buf, nr, nil, st); 91 | n += r; 92 | } 93 | sha1(nil, 0, hcomp->h, st); 94 | if(readn(fd, hexpect.h, sizeof(hexpect.h)) != sizeof(hexpect.h)) 95 | sysfatal("truncated packfile"); 96 | if(!hasheq(hcomp, &hexpect)){ 97 | werrstr("bad hash: %H != %H", *hcomp, hexpect); 98 | return -1; 99 | } 100 | return 0; 101 | } 102 | 103 | int 104 | mkoutpath(char *path) 105 | { 106 | char s[128]; 107 | char *p; 108 | int fd; 109 | 110 | snprint(s, sizeof(s), "%s", path); 111 | for(p=strchr(s+1, '/'); p; p=strchr(p+1, '/')){ 112 | *p = 0; 113 | if(access(s, AEXIST) != 0){ 114 | fd = create(s, OREAD, DMDIR | 0755); 115 | if(fd == -1) 116 | return -1; 117 | close(fd); 118 | } 119 | *p = '/'; 120 | } 121 | return 0; 122 | } 123 | 124 | int 125 | branchmatch(char *br, char *pat) 126 | { 127 | char name[128]; 128 | 129 | if(strstr(pat, "refs/heads") == pat) 130 | snprint(name, sizeof(name), "%s", pat); 131 | else if(strstr(pat, "heads")) 132 | snprint(name, sizeof(name), "refs/%s", pat); 133 | else 134 | snprint(name, sizeof(name), "refs/heads/%s", pat); 135 | return strcmp(br, name) == 0; 136 | } 137 | 138 | char * 139 | matchcap(char *s, char *cap, int full) 140 | { 141 | if(strncmp(s, cap, strlen(cap)) == 0) 142 | if(!full || strlen(s) == strlen(cap)) 143 | return s + strlen(cap); 144 | return nil; 145 | } 146 | 147 | void 148 | handlecaps(char *caps) 149 | { 150 | char *p, *n, *c, *r; 151 | 152 | for(p = caps; p != nil; p = n){ 153 | n = strchr(p, ' '); 154 | if(n != nil) 155 | *n++ = 0; 156 | if((c = matchcap(p, "symref=", 0)) != nil){ 157 | if((r = strchr(c, ':')) != nil){ 158 | *r++ = '\0'; 159 | print("symref %s %s\n", c, r); 160 | } 161 | } 162 | } 163 | } 164 | 165 | int 166 | fetchpack(Conn *c, int pfd, char *packtmp) 167 | { 168 | char buf[Pktmax], idxtmp[256], *sp[3]; 169 | Hash h, *have, *want; 170 | int nref, refsz, first; 171 | int i, n, req; 172 | vlong packsz; 173 | Object *o; 174 | 175 | nref = 0; 176 | refsz = 16; 177 | first = 1; 178 | have = eamalloc(refsz, sizeof(have[0])); 179 | want = eamalloc(refsz, sizeof(want[0])); 180 | while(1){ 181 | n = readpkt(c, buf, sizeof(buf)); 182 | if(n == -1) 183 | return -1; 184 | if(n == 0) 185 | break; 186 | if(strncmp(buf, "ERR ", 4) == 0) 187 | sysfatal("%s", buf + 4); 188 | 189 | if(first && n > strlen(buf)) 190 | handlecaps(buf + strlen(buf) + 1); 191 | first = 0; 192 | 193 | getfields(buf, sp, nelem(sp), 1, " \t\n\r"); 194 | if(strstr(sp[1], "^{}")) 195 | continue; 196 | if(fetchbranch && !branchmatch(sp[1], fetchbranch)) 197 | continue; 198 | if(refsz == nref + 1){ 199 | refsz *= 2; 200 | have = erealloc(have, refsz * sizeof(have[0])); 201 | want = erealloc(want, refsz * sizeof(want[0])); 202 | } 203 | if(hparse(&want[nref], sp[0]) == -1) 204 | sysfatal("invalid hash %s", sp[0]); 205 | if (resolveremote(&have[nref], sp[1]) == -1) 206 | memset(&have[nref], 0, sizeof(have[nref])); 207 | print("remote %s %H local %H\n", sp[1], want[nref], have[nref]); 208 | nref++; 209 | } 210 | if(listonly){ 211 | flushpkt(c); 212 | return 0; 213 | } 214 | 215 | if(writephase(c) == -1) 216 | sysfatal("write: %r"); 217 | req = 0; 218 | for(i = 0; i < nref; i++){ 219 | if(hasheq(&have[i], &want[i])) 220 | continue; 221 | if((o = readobject(want[i])) != nil){ 222 | unref(o); 223 | continue; 224 | } 225 | n = snprint(buf, sizeof(buf), "want %H\n", want[i]); 226 | if(writepkt(c, buf, n) == -1) 227 | sysfatal("could not send want for %H", want[i]); 228 | req = 1; 229 | } 230 | flushpkt(c); 231 | for(i = 0; i < nref; i++){ 232 | if(hasheq(&have[i], &Zhash)) 233 | continue; 234 | n = snprint(buf, sizeof(buf), "have %H\n", have[i]); 235 | if(writepkt(c, buf, n + 1) == -1) 236 | sysfatal("could not send have for %H", have[i]); 237 | } 238 | if(!req) 239 | flushpkt(c); 240 | 241 | n = snprint(buf, sizeof(buf), "done\n"); 242 | if(writepkt(c, buf, n) == -1) 243 | sysfatal("write: %r"); 244 | if(!req) 245 | return 0; 246 | if(readphase(c) == -1) 247 | sysfatal("read: %r"); 248 | if((n = readpkt(c, buf, sizeof(buf))) == -1) 249 | sysfatal("read: %r"); 250 | buf[n] = 0; 251 | 252 | fprint(2, "fetching...\n"); 253 | packsz = 0; 254 | while(1){ 255 | n = readn(c->rfd, buf, sizeof buf); 256 | if(n == 0) 257 | break; 258 | if(n == -1 || write(pfd, buf, n) != n) 259 | sysfatal("fetch packfile: %r"); 260 | packsz += n; 261 | } 262 | closeconn(c); 263 | if(seek(pfd, 0, 0) == -1) 264 | sysfatal("packfile seek: %r"); 265 | if(checkhash(pfd, packsz, &h) == -1) 266 | sysfatal("corrupt packfile: %r"); 267 | close(pfd); 268 | n = strlen(packtmp) - strlen(".tmp"); 269 | memcpy(idxtmp, packtmp, n); 270 | memcpy(idxtmp + n, ".idx", strlen(".idx") + 1); 271 | if(indexpack(packtmp, idxtmp, h) == -1) 272 | sysfatal("could not index fetched pack: %r"); 273 | if(rename(packtmp, idxtmp, h) == -1) 274 | sysfatal("could not rename indexed pack: %r"); 275 | return 0; 276 | } 277 | 278 | void 279 | usage(void) 280 | { 281 | fprint(2, "usage: %s [-dl] [-b br] [-u upstream] remote\n", argv0); 282 | fprint(2, "\t-b br: only fetch matching branch 'br'\n"); 283 | fprint(2, "remote: fetch from this repository\n"); 284 | exits("usage"); 285 | } 286 | 287 | void 288 | main(int argc, char **argv) 289 | { 290 | int pfd; 291 | Conn c; 292 | 293 | ARGBEGIN{ 294 | case 'b': fetchbranch=EARGF(usage()); break; 295 | case 'u': upstream=EARGF(usage()); break; 296 | case 'd': chattygit++; break; 297 | case 'l': listonly++; break; 298 | default: usage(); break; 299 | }ARGEND; 300 | 301 | gitinit(); 302 | if(argc != 1) 303 | usage(); 304 | 305 | if(mkoutpath(packtmp) == -1) 306 | sysfatal("could not create %s: %r", packtmp); 307 | if((pfd = create(packtmp, ORDWR, 0644)) == -1) 308 | sysfatal("could not create %s: %r", packtmp); 309 | 310 | if(gitconnect(&c, argv[0], "upload") == -1) 311 | sysfatal("could not dial %s: %r", argv[0]); 312 | if(fetchpack(&c, pfd, packtmp) == -1) 313 | sysfatal("fetch failed: %r"); 314 | closeconn(&c); 315 | exits(nil); 316 | } 317 | -------------------------------------------------------------------------------- /fs.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include <9p.h> 7 | 8 | #include "git.h" 9 | 10 | enum { 11 | Qroot, 12 | Qhead, 13 | Qbranch, 14 | Qcommit, 15 | Qmsg, 16 | Qparent, 17 | Qtree, 18 | Qcdata, 19 | Qhash, 20 | Qauthor, 21 | Qcommitter, 22 | Qobject, 23 | Qctl, 24 | Qmax, 25 | Internal=1<<7, 26 | }; 27 | 28 | typedef struct Gitaux Gitaux; 29 | typedef struct Crumb Crumb; 30 | typedef struct Cache Cache; 31 | typedef struct Uqid Uqid; 32 | struct Crumb { 33 | char *name; 34 | Object *obj; 35 | Qid qid; 36 | int mode; 37 | vlong mtime; 38 | }; 39 | 40 | struct Gitaux { 41 | int ncrumb; 42 | Crumb *crumb; 43 | char *refpath; 44 | int qdir; 45 | 46 | /* For listing object dir */ 47 | Objlist *ols; 48 | Object *olslast; 49 | }; 50 | 51 | struct Uqid { 52 | vlong uqid; 53 | 54 | vlong ppath; 55 | vlong oid; 56 | int t; 57 | int idx; 58 | }; 59 | 60 | struct Cache { 61 | Uqid *cache; 62 | int n; 63 | int max; 64 | }; 65 | 66 | char *qroot[] = { 67 | "HEAD", 68 | "branch", 69 | "object", 70 | "ctl", 71 | }; 72 | 73 | #define Eperm "permission denied" 74 | #define Eexist "file does not exist" 75 | #define E2long "path too long" 76 | #define Enodir "not a directory" 77 | #define Erepo "unable to read repo" 78 | #define Egreg "wat" 79 | #define Ebadobj "invalid object" 80 | 81 | char gitdir[512]; 82 | char *username; 83 | char *groupname; 84 | char *mntpt = ".git/fs"; 85 | char **branches = nil; 86 | Cache uqidcache[512]; 87 | vlong nextqid = Qmax; 88 | 89 | static Object* walklink(Gitaux *, char *, int, int, int*); 90 | 91 | vlong 92 | qpath(Crumb *p, int idx, vlong id, vlong t) 93 | { 94 | int h, i; 95 | vlong pp; 96 | Cache *c; 97 | Uqid *u; 98 | 99 | pp = p ? p->qid.path : 0; 100 | h = (pp*333 + id*7 + t) & (nelem(uqidcache) - 1); 101 | c = &uqidcache[h]; 102 | u = c->cache; 103 | for(i=0; i n ; i++){ 104 | if(u->ppath == pp && u->oid == id && u->t == t && u->idx == idx) 105 | return (u->uqid << 8) | t; 106 | u++; 107 | } 108 | if(c->n == c->max){ 109 | c->max += c->max/2 + 1; 110 | c->cache = erealloc(c->cache, c->max*sizeof(Uqid)); 111 | } 112 | nextqid++; 113 | c->cache[c->n] = (Uqid){nextqid, pp, id, t, idx}; 114 | c->n++; 115 | return (nextqid << 8) | t; 116 | } 117 | 118 | static Crumb* 119 | crumb(Gitaux *aux, int n) 120 | { 121 | if(n < aux->ncrumb) 122 | return &aux->crumb[aux->ncrumb - n - 1]; 123 | return nil; 124 | } 125 | 126 | static void 127 | popcrumb(Gitaux *aux) 128 | { 129 | Crumb *c; 130 | 131 | if(aux->ncrumb > 1){ 132 | c = crumb(aux, 0); 133 | free(c->name); 134 | unref(c->obj); 135 | aux->ncrumb--; 136 | } 137 | } 138 | 139 | static vlong 140 | branchid(Gitaux *aux, char *path) 141 | { 142 | int i; 143 | 144 | for(i = 0; branches[i]; i++) 145 | if(strcmp(path, branches[i]) == 0) 146 | goto found; 147 | branches = realloc(branches, sizeof(char *)*(i + 2)); 148 | branches[i] = estrdup(path); 149 | branches[i + 1] = nil; 150 | 151 | found: 152 | if(aux){ 153 | if(aux->refpath) 154 | free(aux->refpath); 155 | aux->refpath = estrdup(branches[i]); 156 | } 157 | return i; 158 | } 159 | 160 | static void 161 | obj2dir(Dir *d, Crumb *c, Object *o, char *name) 162 | { 163 | d->qid = c->qid; 164 | d->atime = c->mtime; 165 | d->mtime = c->mtime; 166 | d->mode = c->mode; 167 | d->name = estrdup9p(name); 168 | d->uid = estrdup9p(username); 169 | d->gid = estrdup9p(groupname); 170 | d->muid = estrdup9p(username); 171 | if(o->type == GBlob || o->type == GTag){ 172 | d->qid.type = 0; 173 | d->mode &= 0777; 174 | d->length = o->size; 175 | } 176 | 177 | } 178 | 179 | static int 180 | rootgen(int i, Dir *d, void *p) 181 | { 182 | Crumb *c; 183 | 184 | c = crumb(p, 0); 185 | if (i >= nelem(qroot)) 186 | return -1; 187 | d->mode = 0555 | DMDIR; 188 | d->name = estrdup9p(qroot[i]); 189 | d->qid.vers = 0; 190 | d->qid.type = strcmp(qroot[i], "ctl") == 0 ? 0 : QTDIR; 191 | d->qid.path = qpath(nil, i, i, Qroot); 192 | d->uid = estrdup9p(username); 193 | d->gid = estrdup9p(groupname); 194 | d->muid = estrdup9p(username); 195 | d->mtime = c->mtime; 196 | return 0; 197 | } 198 | 199 | static int 200 | branchgen(int i, Dir *d, void *p) 201 | { 202 | Gitaux *aux; 203 | Dir *refs; 204 | Crumb *c; 205 | int n; 206 | 207 | aux = p; 208 | c = crumb(aux, 0); 209 | refs = nil; 210 | d->qid.vers = 0; 211 | d->qid.type = QTDIR; 212 | d->qid.path = qpath(c, i, branchid(aux, aux->refpath), Qbranch | Internal); 213 | d->mode = 0555 | DMDIR; 214 | d->uid = estrdup9p(username); 215 | d->gid = estrdup9p(groupname); 216 | d->muid = estrdup9p(username); 217 | d->mtime = c->mtime; 218 | d->atime = c->mtime; 219 | if((n = slurpdir(aux->refpath, &refs)) < 0) 220 | return -1; 221 | if(i < n){ 222 | d->name = estrdup9p(refs[i].name); 223 | free(refs); 224 | return 0; 225 | }else{ 226 | free(refs); 227 | return -1; 228 | } 229 | } 230 | 231 | static int 232 | gtreegen(int i, Dir *d, void *p) 233 | { 234 | Object *o, *l, *e; 235 | Gitaux *aux; 236 | Crumb *c; 237 | int m; 238 | 239 | aux = p; 240 | c = crumb(aux, 0); 241 | e = c->obj; 242 | if(i >= e->tree->nent) 243 | return -1; 244 | m = e->tree->ent[i].mode; 245 | if(e->tree->ent[i].ismod) 246 | o = emptydir(); 247 | else if((o = readobject(e->tree->ent[i].h)) == nil) 248 | sysfatal("could not read object %H: %r", e->tree->ent[i].h); 249 | if(e->tree->ent[i].islink) 250 | if((l = walklink(aux, o->data, o->size, 0, &m)) != nil) 251 | o = l; 252 | d->qid.vers = 0; 253 | d->qid.type = o->type == GTree ? QTDIR : 0; 254 | d->qid.path = qpath(c, i, o->id, aux->qdir); 255 | d->mode = m; 256 | d->atime = c->mtime; 257 | d->mtime = c->mtime; 258 | d->uid = estrdup9p(username); 259 | d->gid = estrdup9p(groupname); 260 | d->muid = estrdup9p(username); 261 | d->name = estrdup9p(e->tree->ent[i].name); 262 | d->length = o->size; 263 | return 0; 264 | } 265 | 266 | static int 267 | gcommitgen(int i, Dir *d, void *p) 268 | { 269 | Object *o; 270 | Crumb *c; 271 | 272 | c = crumb(p, 0); 273 | o = c->obj; 274 | d->uid = estrdup9p(username); 275 | d->gid = estrdup9p(groupname); 276 | d->muid = estrdup9p(username); 277 | d->mode = 0444; 278 | d->atime = o->commit->ctime; 279 | d->mtime = o->commit->ctime; 280 | d->qid.type = 0; 281 | d->qid.vers = 0; 282 | 283 | switch(i){ 284 | case 0: 285 | d->mode = 0755 | DMDIR; 286 | d->name = estrdup9p("tree"); 287 | d->qid.type = QTDIR; 288 | d->qid.path = qpath(c, i, o->id, Qtree); 289 | break; 290 | case 1: 291 | d->name = estrdup9p("parent"); 292 | d->qid.path = qpath(c, i, o->id, Qparent); 293 | break; 294 | case 2: 295 | d->name = estrdup9p("msg"); 296 | d->qid.path = qpath(c, i, o->id, Qmsg); 297 | break; 298 | case 3: 299 | d->name = estrdup9p("hash"); 300 | d->qid.path = qpath(c, i, o->id, Qhash); 301 | break; 302 | case 4: 303 | d->name = estrdup9p("author"); 304 | d->qid.path = qpath(c, i, o->id, Qauthor); 305 | break; 306 | default: 307 | free(d->uid); 308 | free(d->gid); 309 | free(d->muid); 310 | return -1; 311 | } 312 | return 0; 313 | } 314 | 315 | 316 | static int 317 | objgen(int i, Dir *d, void *p) 318 | { 319 | Gitaux *aux; 320 | Object *o; 321 | Crumb *c; 322 | char name[64]; 323 | Objlist *ols; 324 | Hash h; 325 | 326 | aux = p; 327 | c = crumb(aux, 0); 328 | if(!aux->ols) 329 | aux->ols = mkols(); 330 | ols = aux->ols; 331 | o = nil; 332 | /* We tried to sent it, but it didn't fit */ 333 | if(aux->olslast && ols->idx == i + 1){ 334 | snprint(name, sizeof(name), "%H", aux->olslast->hash); 335 | obj2dir(d, c, aux->olslast, name); 336 | return 0; 337 | } 338 | while(ols->idx <= i){ 339 | if(olsnext(ols, &h) == -1) 340 | return -1; 341 | if((o = readobject(h)) == nil){ 342 | fprint(2, "corrupt object %H\n", h); 343 | return -1; 344 | } 345 | } 346 | if(o != nil){ 347 | snprint(name, sizeof(name), "%H", o->hash); 348 | obj2dir(d, c, o, name); 349 | unref(aux->olslast); 350 | aux->olslast = ref(o); 351 | return 0; 352 | } 353 | return -1; 354 | } 355 | 356 | static void 357 | objread(Req *r, Gitaux *aux) 358 | { 359 | Object *o; 360 | 361 | o = crumb(aux, 0)->obj; 362 | switch(o->type){ 363 | case GBlob: 364 | readbuf(r, o->data, o->size); 365 | break; 366 | case GTag: 367 | readbuf(r, o->data, o->size); 368 | break; 369 | case GTree: 370 | dirread9p(r, gtreegen, aux); 371 | break; 372 | case GCommit: 373 | dirread9p(r, gcommitgen, aux); 374 | break; 375 | default: 376 | sysfatal("invalid object type %d", o->type); 377 | } 378 | } 379 | 380 | static void 381 | readcommitparent(Req *r, Object *o) 382 | { 383 | char *buf, *p, *e; 384 | int i, n; 385 | 386 | /* 40 bytes per hash, 1 per nl, 1 for terminator */ 387 | n = o->commit->nparent * (40 + 1) + 1; 388 | buf = emalloc(n); 389 | p = buf; 390 | e = buf + n; 391 | for (i = 0; i < o->commit->nparent; i++) 392 | p = seprint(p, e, "%H\n", o->commit->parent[i]); 393 | readbuf(r, buf, p - buf); 394 | free(buf); 395 | } 396 | 397 | static void 398 | gitattach(Req *r) 399 | { 400 | Gitaux *aux; 401 | Dir *d; 402 | 403 | if((d = dirstat(".git")) == nil) 404 | sysfatal("git/fs: %r"); 405 | if(getwd(gitdir, sizeof(gitdir)) == nil) 406 | sysfatal("getwd: %r"); 407 | aux = emalloc(sizeof(Gitaux)); 408 | aux->crumb = emalloc(sizeof(Crumb)); 409 | aux->crumb[0].qid = (Qid){Qroot, 0, QTDIR}; 410 | aux->crumb[0].obj = nil; 411 | aux->crumb[0].mode = DMDIR | 0555; 412 | aux->crumb[0].mtime = d->mtime; 413 | aux->crumb[0].name = estrdup("/"); 414 | aux->ncrumb = 1; 415 | r->ofcall.qid = (Qid){Qroot, 0, QTDIR}; 416 | r->fid->qid = r->ofcall.qid; 417 | r->fid->aux = aux; 418 | respond(r, nil); 419 | } 420 | 421 | static Object* 422 | walklink(Gitaux *aux, char *link, int nlink, int ndotdot, int *mode) 423 | { 424 | char *p, *e, *path; 425 | Object *o, *n; 426 | int i; 427 | 428 | path = emalloc(nlink + 1); 429 | memcpy(path, link, nlink); 430 | cleanname(path); 431 | 432 | o = crumb(aux, ndotdot)->obj; 433 | assert(o->type == GTree); 434 | for(p = path; *p; p = e){ 435 | n = nil; 436 | e = p + strcspn(p, "/"); 437 | if(*e == '/') 438 | *e++ = '\0'; 439 | /* 440 | * cleanname guarantees these show up at the start of the name, 441 | * which allows trimming them from the end of the trail of crumbs 442 | * instead of needing to keep track of full parentage. 443 | */ 444 | if(strcmp(p, "..") == 0) 445 | n = crumb(aux, ++ndotdot)->obj; 446 | else if(o->type == GTree) 447 | for(i = 0; i < o->tree->nent; i++) 448 | if(strcmp(o->tree->ent[i].name, p) == 0){ 449 | *mode = o->tree->ent[i].mode; 450 | n = readobject(o->tree->ent[i].h); 451 | break; 452 | } 453 | o = n; 454 | if(o == nil) 455 | break; 456 | } 457 | free(path); 458 | for(i = 0; o != nil && i < aux->ncrumb; i++) 459 | if(crumb(aux, i)->obj == o) 460 | return nil; 461 | return o; 462 | } 463 | 464 | static char * 465 | objwalk1(Qid *q, Object *o, Crumb *p, Crumb *c, char *name, vlong qdir, Gitaux *aux) 466 | { 467 | Object *w, *l; 468 | char *e; 469 | int i, m; 470 | 471 | w = nil; 472 | e = nil; 473 | if(!o) 474 | return Eexist; 475 | if(o->type == GTree){ 476 | q->type = 0; 477 | for(i = 0; i < o->tree->nent; i++){ 478 | if(strcmp(o->tree->ent[i].name, name) != 0) 479 | continue; 480 | m = o->tree->ent[i].mode; 481 | w = readobject(o->tree->ent[i].h); 482 | if(!w && o->tree->ent[i].ismod) 483 | w = emptydir(); 484 | if(w && o->tree->ent[i].islink) 485 | if((l = walklink(aux, w->data, w->size, 1, &m)) != nil) 486 | w = l; 487 | if(!w) 488 | return Ebadobj; 489 | q->type = (w->type == GTree) ? QTDIR : 0; 490 | q->path = qpath(p, i, w->id, qdir); 491 | c->mode = m; 492 | c->mode |= (w->type == GTree) ? DMDIR|0755 : 0644; 493 | c->obj = w; 494 | break; 495 | } 496 | if(!w) 497 | e = Eexist; 498 | }else if(o->type == GCommit){ 499 | q->type = 0; 500 | c->mtime = o->commit->mtime; 501 | c->mode = 0644; 502 | assert(qdir == Qcommit || qdir == Qobject || qdir == Qtree || qdir == Qhead || qdir == Qcommitter); 503 | if(strcmp(name, "msg") == 0) 504 | q->path = qpath(p, 0, o->id, Qmsg); 505 | else if(strcmp(name, "parent") == 0) 506 | q->path = qpath(p, 1, o->id, Qparent); 507 | else if(strcmp(name, "hash") == 0) 508 | q->path = qpath(p, 2, o->id, Qhash); 509 | else if(strcmp(name, "author") == 0) 510 | q->path = qpath(p, 3, o->id, Qauthor); 511 | else if(strcmp(name, "committer") == 0) 512 | q->path = qpath(p, 3, o->id, Qcommitter); 513 | else if(strcmp(name, "tree") == 0){ 514 | q->type = QTDIR; 515 | q->path = qpath(p, 4, o->id, Qtree); 516 | unref(c->obj); 517 | c->mode = DMDIR | 0755; 518 | c->obj = readobject(o->commit->tree); 519 | if(c->obj == nil) 520 | sysfatal("could not read object %H: %r", o->commit->tree); 521 | } 522 | else 523 | e = Eexist; 524 | }else if(o->type == GTag){ 525 | e = "tag walk unimplemented"; 526 | } 527 | return e; 528 | } 529 | 530 | static Object * 531 | readref(char *pathstr) 532 | { 533 | char buf[128], path[128], *p, *e; 534 | Hash h; 535 | int n, f; 536 | 537 | snprint(path, sizeof(path), "%s", pathstr); 538 | while(1){ 539 | if((f = open(path, OREAD)) == -1) 540 | return nil; 541 | if((n = readn(f, buf, sizeof(buf) - 1)) == -1) 542 | return nil; 543 | close(f); 544 | buf[n] = 0; 545 | if(strncmp(buf, "ref:", 4) != 0) 546 | break; 547 | 548 | p = buf + 4; 549 | while(isspace(*p)) 550 | p++; 551 | if((e = strchr(p, '\n')) != nil) 552 | *e = 0; 553 | snprint(path, sizeof(path), ".git/%s", p); 554 | } 555 | 556 | if(hparse(&h, buf) == -1) 557 | return nil; 558 | 559 | return readobject(h); 560 | } 561 | 562 | static char* 563 | gitwalk1(Fid *fid, char *name, Qid *q) 564 | { 565 | char path[128]; 566 | Gitaux *aux; 567 | Crumb *c, *o; 568 | char *e; 569 | Dir *d; 570 | Hash h; 571 | 572 | e = nil; 573 | aux = fid->aux; 574 | 575 | q->vers = 0; 576 | if(strcmp(name, "..") == 0){ 577 | popcrumb(aux); 578 | c = crumb(aux, 0); 579 | *q = c->qid; 580 | fid->qid = *q; 581 | return nil; 582 | } 583 | 584 | aux->crumb = realloc(aux->crumb, (aux->ncrumb + 1) * sizeof(Crumb)); 585 | aux->ncrumb++; 586 | c = crumb(aux, 0); 587 | o = crumb(aux, 1); 588 | memset(c, 0, sizeof(Crumb)); 589 | c->mode = o->mode; 590 | c->mtime = o->mtime; 591 | c->obj = o->obj ? ref(o->obj) : nil; 592 | 593 | switch(QDIR(&fid->qid)){ 594 | case Qroot: 595 | if(strcmp(name, "HEAD") == 0){ 596 | *q = (Qid){Qhead, 0, QTDIR}; 597 | c->mode = DMDIR | 0555; 598 | c->obj = readref(".git/HEAD"); 599 | }else if(strcmp(name, "object") == 0){ 600 | *q = (Qid){Qobject, 0, QTDIR}; 601 | c->mode = DMDIR | 0555; 602 | }else if(strcmp(name, "branch") == 0){ 603 | *q = (Qid){Qbranch, 0, QTDIR}; 604 | aux->refpath = estrdup(".git/refs/"); 605 | c->mode = DMDIR | 0555; 606 | }else if(strcmp(name, "ctl") == 0){ 607 | *q = (Qid){Qctl, 0, 0}; 608 | c->mode = 0644; 609 | }else{ 610 | e = Eexist; 611 | } 612 | break; 613 | case Qbranch: 614 | if(strcmp(aux->refpath, ".git/refs/heads") == 0 && strcmp(name, "HEAD") == 0) 615 | snprint(path, sizeof(path), ".git/HEAD"); 616 | else 617 | snprint(path, sizeof(path), "%s/%s", aux->refpath, name); 618 | q->type = QTDIR; 619 | d = dirstat(path); 620 | if(d && d->qid.type == QTDIR) 621 | q->path = qpath(o, Qbranch, branchid(aux, path), Qbranch); 622 | else if(d && (c->obj = readref(path)) != nil) 623 | q->path = qpath(o, Qbranch, c->obj->id, Qcommit); 624 | else 625 | e = Eexist; 626 | if(d != nil) 627 | c->mode = d->mode & ~0222; 628 | free(d); 629 | break; 630 | case Qobject: 631 | if(c->obj){ 632 | e = objwalk1(q, o->obj, o, c, name, Qobject, aux); 633 | }else{ 634 | if(hparse(&h, name) == -1) 635 | return Ebadobj; 636 | if((c->obj = readobject(h)) == nil) 637 | return Ebadobj; 638 | if(c->obj->type == GBlob || c->obj->type == GTag){ 639 | c->mode = 0644; 640 | q->type = 0; 641 | }else{ 642 | c->mode = DMDIR | 0755; 643 | q->type = QTDIR; 644 | } 645 | q->path = qpath(o, Qobject, c->obj->id, Qobject); 646 | q->vers = 0; 647 | } 648 | break; 649 | case Qhead: 650 | e = objwalk1(q, o->obj, o, c, name, Qhead, aux); 651 | break; 652 | case Qcommit: 653 | e = objwalk1(q, o->obj, o, c, name, Qcommit, aux); 654 | break; 655 | case Qtree: 656 | e = objwalk1(q, o->obj, o, c, name, Qtree, aux); 657 | break; 658 | case Qparent: 659 | case Qmsg: 660 | case Qcdata: 661 | case Qhash: 662 | case Qauthor: 663 | case Qcommitter: 664 | case Qctl: 665 | return Enodir; 666 | default: 667 | return Egreg; 668 | } 669 | 670 | c->name = estrdup(name); 671 | c->qid = *q; 672 | fid->qid = *q; 673 | return e; 674 | } 675 | 676 | static char* 677 | gitclone(Fid *o, Fid *n) 678 | { 679 | Gitaux *aux, *oaux; 680 | int i; 681 | 682 | oaux = o->aux; 683 | aux = emalloc(sizeof(Gitaux)); 684 | aux->ncrumb = oaux->ncrumb; 685 | aux->crumb = eamalloc(oaux->ncrumb, sizeof(Crumb)); 686 | for(i = 0; i < aux->ncrumb; i++){ 687 | aux->crumb[i] = oaux->crumb[i]; 688 | aux->crumb[i].name = estrdup(oaux->crumb[i].name); 689 | if(aux->crumb[i].obj) 690 | aux->crumb[i].obj = ref(oaux->crumb[i].obj); 691 | } 692 | if(oaux->refpath) 693 | aux->refpath = strdup(oaux->refpath); 694 | aux->qdir = oaux->qdir; 695 | n->aux = aux; 696 | return nil; 697 | } 698 | 699 | static void 700 | gitdestroyfid(Fid *f) 701 | { 702 | Gitaux *aux; 703 | int i; 704 | 705 | if((aux = f->aux) == nil) 706 | return; 707 | for(i = 0; i < aux->ncrumb; i++){ 708 | if(aux->crumb[i].obj) 709 | unref(aux->crumb[i].obj); 710 | free(aux->crumb[i].name); 711 | } 712 | olsfree(aux->ols); 713 | free(aux->refpath); 714 | free(aux->crumb); 715 | free(aux); 716 | } 717 | 718 | static char * 719 | readctl(Req *r) 720 | { 721 | char data[1024], ref[512], *s, *e; 722 | int fd, n; 723 | 724 | if((fd = open(".git/HEAD", OREAD)) == -1) 725 | return Erepo; 726 | /* empty HEAD is invalid */ 727 | if((n = readn(fd, ref, sizeof(ref) - 1)) <= 0) 728 | return Erepo; 729 | close(fd); 730 | 731 | s = ref; 732 | ref[n] = 0; 733 | if(strncmp(s, "ref:", 4) == 0) 734 | s += 4; 735 | while(*s == ' ' || *s == '\t') 736 | s++; 737 | if((e = strchr(s, '\n')) != nil) 738 | *e = 0; 739 | if(strstr(s, "refs/") == s) 740 | s += strlen("refs/"); 741 | 742 | snprint(data, sizeof(data), "branch %s\nrepo %s\n", s, gitdir); 743 | readstr(r, data); 744 | return nil; 745 | } 746 | 747 | static void 748 | gitread(Req *r) 749 | { 750 | char buf[256], *e; 751 | Gitaux *aux; 752 | Object *o; 753 | Qid *q; 754 | 755 | aux = r->fid->aux; 756 | q = &r->fid->qid; 757 | o = crumb(aux, 0)->obj; 758 | e = nil; 759 | 760 | switch(QDIR(q)){ 761 | case Qroot: 762 | dirread9p(r, rootgen, aux); 763 | break; 764 | case Qbranch: 765 | if(o) 766 | objread(r, aux); 767 | else 768 | dirread9p(r, branchgen, aux); 769 | break; 770 | case Qobject: 771 | if(o) 772 | objread(r, aux); 773 | else 774 | dirread9p(r, objgen, aux); 775 | break; 776 | case Qmsg: 777 | readbuf(r, o->commit->msg, o->commit->nmsg); 778 | break; 779 | case Qparent: 780 | readcommitparent(r, o); 781 | break; 782 | case Qhash: 783 | snprint(buf, sizeof(buf), "%H\n", o->hash); 784 | readstr(r, buf); 785 | break; 786 | case Qauthor: 787 | snprint(buf, sizeof(buf), "%s\n", o->commit->author); 788 | readstr(r, buf); 789 | break; 790 | case Qcommitter: 791 | snprint(buf, sizeof(buf), "%s\n", o->commit->committer); 792 | readstr(r, buf); 793 | break; 794 | case Qctl: 795 | e = readctl(r); 796 | break; 797 | case Qhead: 798 | /* Empty repositories have no HEAD */ 799 | if(o == nil) 800 | r->ofcall.count = 0; 801 | else 802 | objread(r, aux); 803 | break; 804 | case Qcommit: 805 | case Qtree: 806 | case Qcdata: 807 | objread(r, aux); 808 | break; 809 | default: 810 | e = Egreg; 811 | } 812 | respond(r, e); 813 | } 814 | 815 | static void 816 | gitopen(Req *r) 817 | { 818 | Gitaux *aux; 819 | Crumb *c; 820 | 821 | aux = r->fid->aux; 822 | c = crumb(aux, 0); 823 | switch(r->ifcall.mode&3){ 824 | default: 825 | respond(r, "botched mode"); 826 | break; 827 | case OWRITE: 828 | respond(r, Eperm); 829 | break; 830 | case OREAD: 831 | case ORDWR: 832 | respond(r, nil); 833 | break; 834 | case OEXEC: 835 | if((c->mode & 0111) == 0) 836 | respond(r, Eperm); 837 | else 838 | respond(r, nil); 839 | break; 840 | } 841 | } 842 | 843 | static void 844 | gitstat(Req *r) 845 | { 846 | Gitaux *aux; 847 | Crumb *c; 848 | 849 | aux = r->fid->aux; 850 | c = crumb(aux, 0); 851 | r->d.uid = estrdup9p(username); 852 | r->d.gid = estrdup9p(groupname); 853 | r->d.muid = estrdup9p(username); 854 | r->d.qid = r->fid->qid; 855 | r->d.mtime = c->mtime; 856 | r->d.atime = c->mtime; 857 | r->d.mode = c->mode; 858 | if(c->obj) 859 | obj2dir(&r->d, c, c->obj, c->name); 860 | else 861 | r->d.name = estrdup9p(c->name); 862 | respond(r, nil); 863 | } 864 | 865 | Srv gitsrv = { 866 | .attach=gitattach, 867 | .walk1=gitwalk1, 868 | .clone=gitclone, 869 | .open=gitopen, 870 | .read=gitread, 871 | .stat=gitstat, 872 | .destroyfid=gitdestroyfid, 873 | }; 874 | 875 | void 876 | usage(void) 877 | { 878 | fprint(2, "usage: %s [-d]\n", argv0); 879 | fprint(2, "\t-d: debug\n"); 880 | exits("usage"); 881 | } 882 | 883 | void 884 | main(int argc, char **argv) 885 | { 886 | Dir *d; 887 | 888 | gitinit(); 889 | ARGBEGIN{ 890 | case 'd': 891 | chatty9p++; 892 | break; 893 | case 'm': 894 | mntpt = EARGF(usage()); 895 | break; 896 | default: 897 | usage(); 898 | break; 899 | }ARGEND; 900 | if(argc != 0) 901 | usage(); 902 | 903 | if((d = dirstat(".git")) == nil) 904 | sysfatal("dirstat .git: %r"); 905 | username = strdup(d->uid); 906 | groupname = strdup(d->gid); 907 | free(d); 908 | 909 | branches = emalloc(sizeof(char*)); 910 | branches[0] = nil; 911 | postmountsrv(&gitsrv, nil, mntpt, MCREATE); 912 | exits(nil); 913 | } 914 | -------------------------------------------------------------------------------- /get.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "git.h" 5 | 6 | char *fetchbranch; 7 | char *upstream = "origin"; 8 | int listonly; 9 | 10 | int 11 | resolveremote(Hash *h, char *ref) 12 | { 13 | char buf[128], *s; 14 | int r, f; 15 | 16 | ref = strip(ref); 17 | if((r = hparse(h, ref)) != -1) 18 | return r; 19 | /* Slightly special handling: translate remote refs to local ones. */ 20 | if(strcmp(ref, "HEAD") == 0){ 21 | snprint(buf, sizeof(buf), ".git/HEAD"); 22 | }else if(strstr(ref, "refs/heads") == ref){ 23 | ref += strlen("refs/heads"); 24 | snprint(buf, sizeof(buf), ".git/refs/remotes/%s/%s", upstream, ref); 25 | }else if(strstr(ref, "refs/tags") == ref){ 26 | ref += strlen("refs/tags"); 27 | snprint(buf, sizeof(buf), ".git/refs/tags/%s/%s", upstream, ref); 28 | }else{ 29 | return -1; 30 | } 31 | 32 | r = -1; 33 | s = strip(buf); 34 | if((f = open(s, OREAD)) == -1) 35 | return -1; 36 | if(readn(f, buf, sizeof(buf)) >= 40) 37 | r = hparse(h, buf); 38 | close(f); 39 | 40 | if(r == -1 && strstr(buf, "ref:") == buf) 41 | return resolveremote(h, buf + strlen("ref:")); 42 | return r; 43 | } 44 | 45 | int 46 | rename(char *pack, char *idx, Hash h) 47 | { 48 | char name[128]; 49 | Dir st; 50 | 51 | nulldir(&st); 52 | st.name = name; 53 | snprint(name, sizeof(name), "%H.pack", h); 54 | if(access(name, AEXIST) == 0) 55 | fprint(2, "warning, pack %s already fetched\n", name); 56 | else if(dirwstat(pack, &st) == -1) 57 | return -1; 58 | snprint(name, sizeof(name), "%H.idx", h); 59 | if(access(name, AEXIST) == 0) 60 | fprint(2, "warning, pack %s already indexed\n", name); 61 | else if(dirwstat(idx, &st) == -1) 62 | return -1; 63 | return 0; 64 | } 65 | 66 | int 67 | checkhash(int fd, vlong sz, Hash *hcomp) 68 | { 69 | DigestState *st; 70 | Hash hexpect; 71 | char buf[Pktmax]; 72 | vlong n, r; 73 | int nr; 74 | 75 | if(sz < 28){ 76 | werrstr("undersize packfile"); 77 | return -1; 78 | } 79 | 80 | st = nil; 81 | n = 0; 82 | while(n != sz - 20){ 83 | nr = sizeof(buf); 84 | if(sz - n - 20 < sizeof(buf)) 85 | nr = sz - n - 20; 86 | r = readn(fd, buf, nr); 87 | if(r != nr) 88 | return -1; 89 | st = sha1((uchar*)buf, nr, nil, st); 90 | n += r; 91 | } 92 | sha1(nil, 0, hcomp->h, st); 93 | if(readn(fd, hexpect.h, sizeof(hexpect.h)) != sizeof(hexpect.h)) 94 | sysfatal("truncated packfile"); 95 | if(!hasheq(hcomp, &hexpect)){ 96 | werrstr("bad hash: %H != %H", *hcomp, hexpect); 97 | return -1; 98 | } 99 | return 0; 100 | } 101 | 102 | int 103 | mkoutpath(char *path) 104 | { 105 | char s[128]; 106 | char *p; 107 | int fd; 108 | 109 | snprint(s, sizeof(s), "%s", path); 110 | for(p=strchr(s+1, '/'); p; p=strchr(p+1, '/')){ 111 | *p = 0; 112 | if(access(s, AEXIST) != 0){ 113 | fd = create(s, OREAD, DMDIR | 0775); 114 | if(fd == -1) 115 | return -1; 116 | close(fd); 117 | } 118 | *p = '/'; 119 | } 120 | return 0; 121 | } 122 | 123 | int 124 | branchmatch(char *br, char *pat) 125 | { 126 | char name[128]; 127 | 128 | if(strstr(pat, "refs/heads") == pat) 129 | snprint(name, sizeof(name), "%s", pat); 130 | else if(strstr(pat, "heads")) 131 | snprint(name, sizeof(name), "refs/%s", pat); 132 | else 133 | snprint(name, sizeof(name), "refs/heads/%s", pat); 134 | return strcmp(br, name) == 0; 135 | } 136 | 137 | char * 138 | matchcap(char *s, char *cap, int full) 139 | { 140 | if(strncmp(s, cap, strlen(cap)) == 0) 141 | if(!full || strlen(s) == strlen(cap)) 142 | return s + strlen(cap); 143 | return nil; 144 | } 145 | 146 | void 147 | handlecaps(char *caps) 148 | { 149 | char *p, *n, *c, *r; 150 | 151 | for(p = caps; p != nil; p = n){ 152 | n = strchr(p, ' '); 153 | if(n != nil) 154 | *n++ = 0; 155 | if((c = matchcap(p, "symref=", 0)) != nil){ 156 | if((r = strchr(c, ':')) != nil){ 157 | *r++ = '\0'; 158 | print("symref %s %s\n", c, r); 159 | } 160 | } 161 | } 162 | } 163 | 164 | void 165 | fail(char *pack, char *idx, char *msg, ...) 166 | { 167 | char buf[ERRMAX]; 168 | va_list ap; 169 | 170 | va_start(ap, msg); 171 | snprint(buf, sizeof(buf), msg, ap); 172 | va_end(ap); 173 | 174 | remove(pack); 175 | remove(idx); 176 | fprint(2, "%s", buf); 177 | exits(buf); 178 | } 179 | 180 | void 181 | enqueueparent(Objq *q, Object *o) 182 | { 183 | Object *p; 184 | int i; 185 | 186 | if(o->type != GCommit) 187 | return; 188 | for(i = 0; i < o->commit->nparent; i++){ 189 | if((p = readobject(o->commit->parent[i])) == nil) 190 | continue; 191 | qput(q, p, 0); 192 | unref(p); 193 | } 194 | } 195 | 196 | int 197 | fetchpack(Conn *c) 198 | { 199 | char buf[Pktmax], *sp[3], *ep; 200 | char *packtmp, *idxtmp, **ref, *caps; 201 | Hash h, *have, *want; 202 | int nref, refsz, first, nsent; 203 | int i, l, n, req, pfd; 204 | vlong packsz; 205 | Objset hadobj; 206 | Object *o; 207 | Objq haveq; 208 | Qelt e; 209 | 210 | nref = 0; 211 | refsz = 16; 212 | first = 1; 213 | have = eamalloc(refsz, sizeof(have[0])); 214 | want = eamalloc(refsz, sizeof(want[0])); 215 | ref = eamalloc(refsz, sizeof(ref[0])); 216 | while(1){ 217 | n = readpkt(c, buf, sizeof(buf)); 218 | if(n == -1) 219 | return -1; 220 | if(n == 0) 221 | break; 222 | 223 | if(first && n > strlen(buf)) 224 | handlecaps(buf + strlen(buf) + 1); 225 | first = 0; 226 | 227 | getfields(buf, sp, nelem(sp), 1, " \t\n\r"); 228 | if(strstr(sp[1], "^{}")) 229 | continue; 230 | if(fetchbranch && !branchmatch(sp[1], fetchbranch)) 231 | continue; 232 | if(refsz == nref + 1){ 233 | refsz *= 2; 234 | have = earealloc(have, refsz, sizeof(have[0])); 235 | want = earealloc(want, refsz, sizeof(want[0])); 236 | ref = earealloc(ref, refsz, sizeof(ref[0])); 237 | } 238 | if(hparse(&want[nref], sp[0]) == -1) 239 | sysfatal("invalid hash %s", sp[0]); 240 | if (resolveremote(&have[nref], sp[1]) == -1) 241 | memset(&have[nref], 0, sizeof(have[nref])); 242 | ref[nref] = estrdup(sp[1]); 243 | nref++; 244 | } 245 | if(listonly){ 246 | flushpkt(c); 247 | goto showrefs; 248 | } 249 | 250 | if(writephase(c) == -1) 251 | sysfatal("write: %r"); 252 | req = 0; 253 | caps = " multi_ack"; 254 | for(i = 0; i < nref; i++){ 255 | if(hasheq(&have[i], &want[i])) 256 | continue; 257 | if((o = readobject(want[i])) != nil){ 258 | unref(o); 259 | continue; 260 | } 261 | if(fmtpkt(c, "want %H%s\n", want[i], caps) == -1) 262 | sysfatal("could not send want for %H", want[i]); 263 | caps = ""; 264 | req = 1; 265 | } 266 | flushpkt(c); 267 | 268 | nsent = 0; 269 | qinit(&haveq); 270 | osinit(&hadobj); 271 | /* 272 | * We know we have these objects, and we want to make sure that 273 | * they end up at the front of the queue. Send the 'have lines' 274 | * first, and then enqueue their parents for a second round of 275 | * sends. 276 | */ 277 | for(i = 0; i < nref; i++){ 278 | if(hasheq(&have[i], &Zhash) || oshas(&hadobj, have[i])) 279 | continue; 280 | if((o = readobject(have[i])) == nil) 281 | sysfatal("missing exected object: %H", have[i]); 282 | if(fmtpkt(c, "have %H", o->hash) == -1) 283 | sysfatal("write: %r"); 284 | enqueueparent(&haveq, o); 285 | osadd(&hadobj, o); 286 | unref(o); 287 | } 288 | /* 289 | * While we could short circuit this and check if upstream has 290 | * acked our objects, for the first 256 haves, this is simple 291 | * enough. 292 | * 293 | * Also, doing multiple rounds of reference discovery breaks 294 | * when using smart http. 295 | */ 296 | while(req && qpop(&haveq, &e) && nsent < 256){ 297 | if(oshas(&hadobj, e.o->hash)) 298 | continue; 299 | if((o = readobject(e.o->hash)) == nil) 300 | sysfatal("missing object we should have: %H", have[i]); 301 | if(fmtpkt(c, "have %H", o->hash) == -1) 302 | sysfatal("write: %r"); 303 | enqueueparent(&haveq, o); 304 | osadd(&hadobj, o); 305 | unref(o); 306 | nsent++; 307 | } 308 | osclear(&hadobj); 309 | qclear(&haveq); 310 | if(!req) 311 | flushpkt(c); 312 | if(fmtpkt(c, "done\n") == -1) 313 | sysfatal("write: %r"); 314 | if(!req) 315 | goto showrefs; 316 | if(readphase(c) == -1) 317 | sysfatal("read: %r"); 318 | if((n = readpkt(c, buf, sizeof(buf))) == -1) 319 | sysfatal("read: %r"); 320 | buf[n] = 0; 321 | 322 | if((packtmp = smprint(".git/objects/pack/fetch.%d.pack", getpid())) == nil) 323 | sysfatal("smprint: %r"); 324 | if((idxtmp = smprint(".git/objects/pack/fetch.%d.idx", getpid())) == nil) 325 | sysfatal("smprint: %r"); 326 | if(mkoutpath(packtmp) == -1) 327 | sysfatal("could not create %s: %r", packtmp); 328 | if((pfd = create(packtmp, ORDWR, 0664)) == -1) 329 | sysfatal("could not create %s: %r", packtmp); 330 | 331 | fprint(2, "fetching...\n"); 332 | /* 333 | * Work around torvalds git bug: we get duplicate have lines 334 | * somtimes, even though the protocol is supposed to start the 335 | * pack file immediately. 336 | * 337 | * Skip ahead until we read 'PACK' off the wire 338 | */ 339 | while(1){ 340 | if(readn(c->rfd, buf, 4) != 4) 341 | sysfatal("fetch packfile: short read"); 342 | buf[4] = 0; 343 | if(strncmp(buf, "PACK", 4) == 0) 344 | break; 345 | l = strtol(buf, &ep, 16); 346 | if(ep != buf + 4) 347 | sysfatal("fetch packfile: junk pktline"); 348 | if(readn(c->rfd, buf, l-4) != l-4) 349 | sysfatal("fetch packfile: short read"); 350 | } 351 | if(write(pfd, "PACK", 4) != 4) 352 | sysfatal("write pack header: %r"); 353 | packsz = 4; 354 | while(1){ 355 | n = read(c->rfd, buf, sizeof buf); 356 | if(n == 0) 357 | break; 358 | if(n == -1 || write(pfd, buf, n) != n) 359 | sysfatal("fetch packfile: %r"); 360 | packsz += n; 361 | } 362 | 363 | closeconn(c); 364 | if(seek(pfd, 0, 0) == -1) 365 | fail(packtmp, idxtmp, "packfile seek: %r"); 366 | if(checkhash(pfd, packsz, &h) == -1) 367 | fail(packtmp, idxtmp, "corrupt packfile: %r"); 368 | close(pfd); 369 | if(indexpack(packtmp, idxtmp, h) == -1) 370 | fail(packtmp, idxtmp, "could not index fetched pack: %r"); 371 | if(rename(packtmp, idxtmp, h) == -1) 372 | fail(packtmp, idxtmp, "could not rename indexed pack: %r"); 373 | 374 | showrefs: 375 | for(i = 0; i < nref; i++){ 376 | print("remote %s %H local %H\n", ref[i], want[i], have[i]); 377 | free(ref[i]); 378 | } 379 | free(ref); 380 | free(want); 381 | free(have); 382 | return 0; 383 | } 384 | 385 | void 386 | usage(void) 387 | { 388 | fprint(2, "usage: %s [-dl] [-b br] [-u upstream] remote\n", argv0); 389 | fprint(2, "\t-b br: only fetch matching branch 'br'\n"); 390 | fprint(2, "remote: fetch from this repository\n"); 391 | exits("usage"); 392 | } 393 | 394 | void 395 | main(int argc, char **argv) 396 | { 397 | Conn c; 398 | 399 | ARGBEGIN{ 400 | case 'b': fetchbranch=EARGF(usage()); break; 401 | case 'u': upstream=EARGF(usage()); break; 402 | case 'd': chattygit++; break; 403 | case 'l': listonly++; break; 404 | default: usage(); break; 405 | }ARGEND; 406 | 407 | gitinit(); 408 | if(argc != 1) 409 | usage(); 410 | 411 | if(gitconnect(&c, argv[0], "upload") == -1) 412 | sysfatal("could not dial %s: %r", argv[0]); 413 | if(fetchpack(&c) == -1) 414 | sysfatal("fetch failed: %r"); 415 | closeconn(&c); 416 | exits(nil); 417 | } 418 | -------------------------------------------------------------------------------- /git.1.man: -------------------------------------------------------------------------------- 1 | .TH GIT 1 2 | .SH NAME 3 | git, git/conf, git/query, git/walk, git/clone, git/branch, 4 | git/commit, git/diff, git/init, git/log, git/merge, git/push, 5 | git/pull, git/rm, git/serve 6 | \- Manage git repositories. 7 | 8 | .SH SYNOPSIS 9 | .PP 10 | .B git/add 11 | [ 12 | .B -r 13 | ] 14 | .I path... 15 | .PP 16 | .B git/rm 17 | .I path... 18 | .PP 19 | .B git/branch 20 | [ 21 | .B -admns 22 | ] 23 | [ 24 | .B -b 25 | .I base 26 | ] 27 | .I newbranch 28 | .PP 29 | .B git/clone 30 | [ 31 | .I remote 32 | [ 33 | .I local 34 | ] 35 | ] 36 | .PP 37 | .B git/commit 38 | [ 39 | .B -re 40 | ] 41 | [ 42 | .B -m msg 43 | ] 44 | [ 45 | .I file... 46 | ] 47 | .PP 48 | .B git/compat 49 | .PP 50 | .B git/conf 51 | [ 52 | .B -r 53 | ] 54 | [ 55 | .B -f 56 | .I file 57 | ] 58 | .I keys... 59 | .PP 60 | .B git/diff 61 | [ 62 | .B -c 63 | .I branch 64 | ] 65 | [ 66 | .B -s 67 | ] 68 | [ 69 | .I file... 70 | ] 71 | .PP 72 | .B git/revert 73 | [ 74 | .B -c 75 | .I commit 76 | ] 77 | .I file... 78 | .PP 79 | .B git/export 80 | [ 81 | .I commits... 82 | ] 83 | .PP 84 | .B git/import 85 | [ 86 | .I commits... 87 | ] 88 | .PP 89 | .B git/init 90 | [ 91 | .B -b 92 | ] 93 | [ 94 | .I dir 95 | ] 96 | [ 97 | .B -u 98 | .I upstream 99 | ] 100 | .PP 101 | .B git/log 102 | [ 103 | .B -c 104 | .I commit 105 | .B | -e 106 | .I expr 107 | ] 108 | [ 109 | .B -s 110 | ] 111 | [ 112 | .I files... 113 | ] 114 | .PP 115 | .B git/merge 116 | .I theirs 117 | .PP 118 | .B git/rebase 119 | [ 120 | .B -ari 121 | ] 122 | [ 123 | .B onto 124 | ] 125 | .PP 126 | .B git/pull 127 | [ 128 | .B -f 129 | ] 130 | [ 131 | .B -q 132 | ] 133 | [ 134 | .B -a 135 | ] 136 | [ 137 | .B -u 138 | .I upstream 139 | ] 140 | .PP 141 | .B git/push 142 | [ 143 | .B -a 144 | ] 145 | [ 146 | .B -u 147 | .I upstream 148 | ] 149 | [ 150 | .B -b 151 | .I branch 152 | ] 153 | [ 154 | .B -r 155 | .I branch 156 | ] 157 | .PP 158 | .B git/serve 159 | [ 160 | .B -w 161 | ] 162 | [ 163 | .B -r 164 | .I path 165 | ] 166 | .PP 167 | .B git/query 168 | [ 169 | .B -pcr 170 | ] 171 | .I query 172 | .PP 173 | .B git/walk 174 | [ 175 | .B -qc 176 | ] 177 | [ 178 | .B -b 179 | .I branch 180 | ] 181 | [ 182 | .B -f 183 | .I filters 184 | ] 185 | [ 186 | .I [files...] 187 | ] 188 | 189 | .SH DESCRIPTION 190 | .PP 191 | Git is a distributed version control system. 192 | This means that each repository contains a full copy of the history. 193 | This history is then synced between computers as needed. 194 | 195 | .PP 196 | These programs provide tools to manage and interoperate with 197 | repositories hosted in git. 198 | 199 | .SH CONCEPTS 200 | 201 | Git stores snapshots of the working directory. 202 | Files can either be in a tracked or untracked state. 203 | Each commit takes the current version of all tracked files and 204 | adds them to a new commit. 205 | 206 | This history is stored in the 207 | .I .git 208 | directory. 209 | This suite of 210 | .I git 211 | tools provides a file interface to the 212 | .I .git 213 | directory mounted on 214 | .I $repo/.git/fs. 215 | Modifications to the repository are done directly to the 216 | .I .git 217 | directory, and are reflected in the file system interface. 218 | This allows for easy scripting, without excessive complexity 219 | in the file API. 220 | 221 | .SH COMMANDS 222 | 223 | .PP 224 | .B Git/init 225 | is used to create a new git repository, with no code or commits. 226 | The repository is created in the current directory by default. 227 | Passing a directory name will cause the repository to be created 228 | there instead. 229 | Passing the 230 | .B -b 231 | option will cause the repository to be initialized as a bare repository. 232 | Passing the 233 | .B -u 234 | .I upstream 235 | option will cause the upstream to be configured to 236 | .I upstream. 237 | 238 | .PP 239 | .B Git/clone 240 | will take an existing repository, served over either the 241 | .I git:// 242 | or 243 | .I ssh:// 244 | protocols. 245 | The first argument is the repository to clone. 246 | The second argument, optionally, specifies the location to clone into. 247 | If not specified, the repository will be cloned into the last path component 248 | of the clone source, with the 249 | .I .git 250 | stripped off if present. 251 | 252 | .PP 253 | .B Git/push 254 | is used to push the current changes to a remote repository. 255 | When no arguments are provided, the remote repository is taken from 256 | the origin configured in 257 | .I .git/config, 258 | and only the changes on the current branch are pushed. 259 | When passed the 260 | .I -a 261 | option, all branches are pushed. 262 | When passed the 263 | .I -u upstream 264 | option, the changed are pushed to 265 | .I upstream 266 | instead of the configured origin. 267 | When given the 268 | .I -r 269 | option, the branch is deleted from origin, instead of updated. 270 | 271 | .PP 272 | .B Git/revert 273 | restores the named files from HEAD. When passed the -c flag, restores files from 274 | the named commit. 275 | 276 | .PP 277 | .B Git/pull 278 | behaves in a similar manner to git/push, however it gets changes from 279 | the upstream repository. 280 | After fetching, it checks out the changes into the working directory. 281 | When passed the 282 | .I -f 283 | option, the update of the working copy is suppressed. 284 | When passed the 285 | .I -u upstream 286 | option, the changes are pulled from 287 | .I upstream 288 | instead of the configured origin. 289 | 290 | .PP 291 | .B Git/serve 292 | serves repositories using the 293 | .I git:// 294 | protocol over stdin. 295 | By default, it serves them read-only. 296 | The 297 | .I -w 298 | flag, it allows pushing into repositories. 299 | The 300 | .I -r 301 | .B path 302 | flag serves repositories relative to 303 | .BR path . 304 | 305 | .PP 306 | .B Git/fs 307 | serves a file system on $repo/.git/fs. 308 | For full documentation, see 309 | .IR gitfs (4) 310 | 311 | .PP 312 | .B Git/add 313 | adds a file to the list of tracked files. When passed the 314 | .I -r 315 | flag, the file is removed from the list of tracked files. 316 | The copy of the file in the repository is left untouched. 317 | .PP 318 | .B Git/rm 319 | is an alias for 320 | .IR git/add -r . 321 | 322 | .PP 323 | .B Git/commit 324 | creates a new commit consisting of all changes to the specified files. 325 | By default, an editor is opened to prepare the commit message. 326 | The 327 | .I -m 328 | flag supplies the commit message directly. 329 | The 330 | .I -r 331 | flag revises the contents of the previous commit, reusing the message. 332 | The 333 | .I -e 334 | flag opens an editor to finalize the commit message, regardless of 335 | whether or not it was specified explicitly or reused. 336 | To amend a commit message, 337 | .I -r 338 | can be used in conjuction with 339 | .I -m 340 | or 341 | .IR -e . 342 | 343 | .PP 344 | .B Git/branch 345 | is used to list or switch branches. 346 | When invoked with no arguments, it lists the current branch. 347 | To list all branches, pass the 348 | .I -a 349 | option. 350 | To switch between branches, pass a branch name. 351 | When passed the 352 | .I -n 353 | option, the branch will be created, overwriting existing branch. 354 | When passed the 355 | .I -b base 356 | option, the branch created is based off of 357 | .I base 358 | instead of 359 | .I HEAD. 360 | When passed the 361 | .I -s 362 | option, the branch is created but the files are not checked out. 363 | When passed the 364 | .I -d 365 | option, the branch is deleted. 366 | .PP 367 | When switching branches, git/branch will refuse to clobber 368 | modificiations. 369 | Passing the 370 | .I -m 371 | option will cause git9 to attempt to merge the changes between 372 | the branches. 373 | 374 | .PP 375 | .B Git/log 376 | shows a history of the current branch. 377 | When passed a list of files, only commits affecting 378 | those files are shown. 379 | The 380 | .I -c commit 381 | option logs starting from the provided commit, instead of HEAD. 382 | The 383 | .I -s 384 | option shows a summary of the commit, instead of the full message. 385 | The 386 | .I -e expr 387 | option shows commits matching the query expression provided. 388 | The expression is in the syntax of 389 | .B git/query. 390 | 391 | .PP 392 | .B Git/diff 393 | shows the differences between the currently checked out code and 394 | the 395 | .I HEAD 396 | commit. 397 | When passed the 398 | .I -c base 399 | option, the diff is computed against 400 | .I base 401 | instead of 402 | .I HEAD. 403 | When passed the 404 | .I -s 405 | option, only the file statuses are 406 | printed. 407 | 408 | .PP 409 | .B Git/export 410 | exports a list of commits in a format that 411 | .B git/import 412 | can apply. 413 | 414 | .PP 415 | .B Git/import 416 | imports a commit with message, author, and 417 | date information. 418 | 419 | .PP 420 | .B Git/merge 421 | takes two branches and merges them filewise using 422 | .I ape/diff3. 423 | The next commit made will be a merge commmit. 424 | 425 | .PP 426 | .B Git/rebase 427 | takes one branch and moves it onto another. 428 | On error, the remaining commits to rebase are 429 | saved, and can be resumed once the conflict is 430 | resolved using the 431 | .I -r 432 | option. 433 | If the rebase is to be aborted, the 434 | .I -a 435 | option will clean up the in progress rebase 436 | and reset the state of the branch. 437 | The 438 | .I -i 439 | option will open an editor to modify the todo-list before the rebase 440 | begins. 441 | 442 | .PP 443 | The following rebase commands are supported: 444 | .TP 10 445 | .B pick 446 | Apply the commit. 447 | .TP 448 | .B reword 449 | Apply the commit, then edit its commit message. 450 | .TP 451 | .B edit 452 | Apply the commit, then exit to allow further changes. 453 | .TP 454 | .B squash 455 | Fold the commit into the previous commit, then edit the combined 456 | commit message. 457 | .TP 458 | .B fixup 459 | Fold the commit into the previous commit, discarding its commit 460 | message. 461 | .TP 462 | .B break 463 | Exit to allow for manual edits or inspection before continuing. 464 | 465 | .PP 466 | .B Git/conf 467 | is a tool for querying the git configuration. 468 | The configuration key is provided as a dotted string. Spaces 469 | are accepted. For example, to find the URL of the origin 470 | repository, one might pass 471 | .I 'remote "origin".url". 472 | When given the 473 | .I -r 474 | option, the root of the current repository is printed. 475 | 476 | .B Git/query 477 | takes an expression describing a commit, or set of commits, 478 | and resolves it to a list of commits. 479 | The 480 | .I -r 481 | option reverses the order of the commit list. 482 | With the 483 | .I -p 484 | option, instead of printing the commit hashes, the full 485 | path to their 486 | .B git/fs 487 | path is printed. With the 488 | .I -c 489 | option, the query must resolve to two commits. The blobs 490 | that have changed in the commits are printed. 491 | 492 | .PP 493 | .B Git/walk 494 | provides a tool for walking the list of tracked objects and printing their status. 495 | With no arguments, it prints a list of paths prefixed with the status character. 496 | When given the 497 | .I -c 498 | character, only the paths are printed. 499 | When given the 500 | .I -q 501 | option, all output is suppressed, and only the status is printed. 502 | When given the 503 | .I -f 504 | option, the output is filtered by status code, and only matching items are printed. 505 | 506 | .PP 507 | The status characters are as follows: 508 | .TP 509 | T 510 | Tracked, not modified since last commit. 511 | .TP 512 | M 513 | Modified since last commit. 514 | .TP 515 | R 516 | Removed from either working directory tracking list. 517 | .TP 518 | A 519 | Added, does not yet exist in a commit. 520 | 521 | .PP 522 | .B Git/compat 523 | spawns an rc subshell with a compatibility stub in 524 | .IR $path . 525 | This compatibility stub provides enough of the unix 526 | .I git 527 | commands to run tools like 528 | .I go get 529 | but not much more. 530 | 531 | .SH REF SYNTAX 532 | 533 | .PP 534 | Refs are specified with a simple query syntax. 535 | A bare hash always evaluates to itself. 536 | Ref names are resolved to their hashes. 537 | The 538 | .B a ^ 539 | suffix operator finds the parent of a commit. 540 | The 541 | .B a b @ 542 | suffix operator finds the common ancestor of the previous two commits. 543 | The 544 | .B a .. b 545 | or 546 | .B a : b 547 | operator finds all commits between 548 | .B a 549 | and 550 | .B b. 551 | Between is defined as the set of all commits which are reachable from 552 | .B b 553 | but not reachable from 554 | .B a. 555 | 556 | .SH PROTOCOLS 557 | .PP 558 | Git9 supports URL schemes of the format 559 | .BR transport://dial/repo/path . 560 | The transport portion specifies the protocol to use. 561 | If the transport portion is omitted, then the transport used is 562 | .BR ssh . 563 | The 564 | .I dial 565 | portion is either a plan 9 dial string, or a conventional 566 | .I host:port 567 | pair. 568 | For the ssh protocol, it may also include a 569 | .I user@ 570 | prefix. 571 | .I repo/path 572 | portion is the path of the repository on the server. 573 | 574 | The supported transports are 575 | .B ssh://, git://, hjgit://, gits://, http://, 576 | and 577 | .BR https . 578 | Two of these are specific to git9: 579 | .I gits:// 580 | and 581 | .IR hjgit:// . 582 | Both are the 583 | .I git:// 584 | protocol, tunnelled over tls. 585 | .I Hjgit:// 586 | authenticates with the server using Plan 9 authentication, 587 | using 588 | .IR tlsclient\ -a . 589 | Any of these protocol names may be prefixed with 590 | .IR git+ , 591 | for copy-paste compatibility with Unix git. 592 | 593 | .SH EXAMPLES 594 | 595 | .PP 596 | In order to create a new repository, run 597 | .B git/init: 598 | .PP 599 | .EX 600 | git/init myrepo 601 | .EE 602 | 603 | .PP 604 | To clone an existing repository from a git server, run: 605 | .PP 606 | .EX 607 | git/clone git://github.com/Harvey-OS/harvey 608 | cd harvey 609 | # edit files 610 | git/commit foo.c 611 | git/push 612 | .EE 613 | 614 | .PP 615 | To set a user and email for commits, run: 616 | .PP 617 | .EX 618 | % mkdir $home/lib/git 619 | % >$home/lib/git/config echo ' 620 | [user] 621 | name = Ori Bernstein 622 | email = ori@eigenstate.org' 623 | .EE 624 | 625 | .SH FILES 626 | .TP 627 | $repo/.git 628 | The full git repository. 629 | .TP 630 | $repo/.git/config 631 | The configuration file for a repository. 632 | .TP 633 | $home/lib/git/config 634 | The global configuration for git. 635 | The contents of this file are used as fallbacks for the per-repository config. 636 | 637 | .SH SEE ALSO 638 | .IR hg (1) 639 | .IR replica (1) 640 | .IR patch (1) 641 | .IR gitfs (4) 642 | .IR diff3 643 | 644 | .SH BUGS 645 | .PP 646 | Repositories with submodules are effectively read-only. 647 | .PP 648 | There are a some of missing commands, features, and tools, such as git/rebase 649 | .PP 650 | git/compat only works within a git repository. 651 | -------------------------------------------------------------------------------- /git.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | typedef struct Capset Capset; 8 | typedef struct Conn Conn; 9 | typedef struct Hash Hash; 10 | typedef struct Delta Delta; 11 | typedef struct Cinfo Cinfo; 12 | typedef struct Tinfo Tinfo; 13 | typedef struct Object Object; 14 | typedef struct Objset Objset; 15 | typedef struct Pack Pack; 16 | typedef struct Buf Buf; 17 | typedef struct Dirent Dirent; 18 | typedef struct Idxent Idxent; 19 | typedef struct Objlist Objlist; 20 | typedef struct Dtab Dtab; 21 | typedef struct Dblock Dblock; 22 | typedef struct Objq Objq; 23 | typedef struct Qelt Qelt; 24 | typedef struct Idxent Idxent; 25 | 26 | enum { 27 | Pathmax = 512, 28 | Npackcache = 32, 29 | Hashsz = 20, 30 | Pktmax = 65536, 31 | KiB = 1024, 32 | MiB = 1024*KiB, 33 | }; 34 | 35 | enum { 36 | GNone = 0, 37 | GCommit = 1, 38 | GTree = 2, 39 | GBlob = 3, 40 | GTag = 4, 41 | GOdelta = 6, 42 | GRdelta = 7, 43 | }; 44 | 45 | enum { 46 | Cloaded = 1 << 0, 47 | Cidx = 1 << 1, 48 | Ccache = 1 << 2, 49 | Cexist = 1 << 3, 50 | Cparsed = 1 << 5, 51 | Cthin = 1 << 6, 52 | }; 53 | 54 | enum { 55 | ConnGit, 56 | ConnGit9, 57 | ConnSsh, 58 | ConnHttp, 59 | }; 60 | 61 | struct Objlist { 62 | int idx; 63 | 64 | int fd; 65 | int state; 66 | int stage; 67 | 68 | Dir *top; 69 | int ntop; 70 | int topidx; 71 | Dir *loose; 72 | int nloose; 73 | int looseidx; 74 | Dir *pack; 75 | int npack; 76 | int packidx; 77 | int nent; 78 | int entidx; 79 | }; 80 | 81 | struct Hash { 82 | uchar h[20]; 83 | }; 84 | 85 | struct Conn { 86 | int type; 87 | int rfd; 88 | int wfd; 89 | 90 | /* only used by http */ 91 | int cfd; 92 | char *url; /* note, first GET uses a different url */ 93 | char *dir; 94 | char *direction; 95 | }; 96 | 97 | struct Dirent { 98 | char *name; 99 | int mode; 100 | Hash h; 101 | char ismod; 102 | char islink; 103 | }; 104 | 105 | struct Object { 106 | /* Git data */ 107 | Hash hash; 108 | int type; 109 | 110 | /* Cache */ 111 | int id; 112 | int flag; 113 | int refs; 114 | Object *next; 115 | Object *prev; 116 | 117 | /* For indexing */ 118 | vlong off; 119 | vlong len; 120 | u32int crc; 121 | 122 | /* Everything below here gets cleared */ 123 | char *all; 124 | char *data; 125 | /* size excludes header */ 126 | vlong size; 127 | 128 | /* Significant win on memory use */ 129 | union { 130 | Cinfo *commit; 131 | Tinfo *tree; 132 | }; 133 | }; 134 | 135 | struct Tinfo { 136 | /* Tree */ 137 | Dirent *ent; 138 | int nent; 139 | }; 140 | 141 | struct Cinfo { 142 | /* Commit */ 143 | Hash *parent; 144 | int nparent; 145 | Hash tree; 146 | char *author; 147 | char *committer; 148 | char *msg; 149 | int nmsg; 150 | vlong ctime; 151 | vlong mtime; 152 | }; 153 | 154 | struct Objset { 155 | Object **obj; 156 | int nobj; 157 | int sz; 158 | }; 159 | 160 | struct Qelt { 161 | Object *o; 162 | vlong ctime; 163 | int color; 164 | }; 165 | 166 | struct Objq { 167 | Qelt *heap; 168 | int nheap; 169 | int heapsz; 170 | }; 171 | 172 | struct Dtab { 173 | Object *o; 174 | uchar *base; 175 | int nbase; 176 | Dblock *b; 177 | int nb; 178 | int sz; 179 | }; 180 | 181 | struct Dblock { 182 | uchar *buf; 183 | int len; 184 | int off; 185 | u64int hash; 186 | }; 187 | 188 | struct Delta { 189 | int cpy; 190 | int off; 191 | int len; 192 | }; 193 | 194 | struct Idxent { 195 | char *path; 196 | Qid qid; 197 | int mode; 198 | int order; 199 | char state; 200 | }; 201 | 202 | #define GETBE16(b)\ 203 | ((((b)[0] & 0xFFul) << 8) | \ 204 | (((b)[1] & 0xFFul) << 0)) 205 | 206 | #define GETBE32(b)\ 207 | ((((b)[0] & 0xFFul) << 24) | \ 208 | (((b)[1] & 0xFFul) << 16) | \ 209 | (((b)[2] & 0xFFul) << 8) | \ 210 | (((b)[3] & 0xFFul) << 0)) 211 | #define GETBE64(b)\ 212 | ((((b)[0] & 0xFFull) << 56) | \ 213 | (((b)[1] & 0xFFull) << 48) | \ 214 | (((b)[2] & 0xFFull) << 40) | \ 215 | (((b)[3] & 0xFFull) << 32) | \ 216 | (((b)[4] & 0xFFull) << 24) | \ 217 | (((b)[5] & 0xFFull) << 16) | \ 218 | (((b)[6] & 0xFFull) << 8) | \ 219 | (((b)[7] & 0xFFull) << 0)) 220 | 221 | #define PUTBE16(b, n)\ 222 | do{ \ 223 | (b)[0] = (n) >> 8; \ 224 | (b)[1] = (n) >> 0; \ 225 | } while(0) 226 | 227 | #define PUTBE32(b, n)\ 228 | do{ \ 229 | (b)[0] = (n) >> 24; \ 230 | (b)[1] = (n) >> 16; \ 231 | (b)[2] = (n) >> 8; \ 232 | (b)[3] = (n) >> 0; \ 233 | } while(0) 234 | 235 | #define PUTBE64(b, n)\ 236 | do{ \ 237 | (b)[0] = (n) >> 56; \ 238 | (b)[1] = (n) >> 48; \ 239 | (b)[2] = (n) >> 40; \ 240 | (b)[3] = (n) >> 32; \ 241 | (b)[4] = (n) >> 24; \ 242 | (b)[5] = (n) >> 16; \ 243 | (b)[6] = (n) >> 8; \ 244 | (b)[7] = (n) >> 0; \ 245 | } while(0) 246 | 247 | #define QDIR(qid) ((int)(qid)->path & (0xff)) 248 | #define isblank(c) \ 249 | (((c) != '\n') && isspace(c)) 250 | 251 | extern Reprog *authorpat; 252 | extern Objset objcache; 253 | extern vlong cachemax; 254 | extern Hash Zhash; 255 | extern int chattygit; 256 | extern int interactive; 257 | 258 | #pragma varargck type "H" Hash 259 | #pragma varargck type "T" int 260 | #pragma varargck type "O" Object* 261 | #pragma varargck type "Q" Qid 262 | int Hfmt(Fmt*); 263 | int Tfmt(Fmt*); 264 | int Ofmt(Fmt*); 265 | int Qfmt(Fmt*); 266 | 267 | void gitinit(void); 268 | 269 | /* object io */ 270 | int resolverefs(Hash **, char *); 271 | int resolveref(Hash *, char *); 272 | int listrefs(Hash **, char ***); 273 | Object *ancestor(Object *, Object *); 274 | int findtwixt(Hash *, int, Hash *, int, Object ***, int *); 275 | Object *readobject(Hash); 276 | Object *clearedobject(Hash, int); 277 | void parseobject(Object *); 278 | int indexpack(char *, char *, Hash); 279 | int writepack(int, Hash*, int, Hash*, int, Hash*); 280 | int hasheq(Hash *, Hash *); 281 | Object *ref(Object *); 282 | void unref(Object *); 283 | void cache(Object *); 284 | Object *emptydir(void); 285 | 286 | /* object sets */ 287 | void osinit(Objset *); 288 | void osclear(Objset *); 289 | void osadd(Objset *, Object *); 290 | int oshas(Objset *, Hash); 291 | Object *osfind(Objset *, Hash); 292 | 293 | /* object listing */ 294 | Objlist *mkols(void); 295 | int olsnext(Objlist *, Hash *); 296 | void olsfree(Objlist *); 297 | 298 | /* util functions */ 299 | #define dprint(lvl, ...) \ 300 | if(chattygit >= lvl) _dprint(__VA_ARGS__) 301 | void _dprint(char *, ...); 302 | void *eamalloc(ulong, ulong); 303 | void *emalloc(ulong); 304 | void *earealloc(void *, ulong, ulong); 305 | void *erealloc(void *, ulong); 306 | char *estrdup(char *); 307 | int slurpdir(char *, Dir **); 308 | int hparse(Hash *, char *); 309 | int hassuffix(char *, char *); 310 | int swapsuffix(char *, int, char *, char *, char *); 311 | char *strip(char *); 312 | int findrepo(char *, int, int*); 313 | int showprogress(int, int); 314 | u64int murmurhash2(void*, usize); 315 | Qid parseqid(char*); 316 | 317 | /* packing */ 318 | void dtinit(Dtab *, Object*); 319 | void dtclear(Dtab*); 320 | Delta* deltify(Object*, Dtab*, int*); 321 | 322 | /* proto handling */ 323 | int readpkt(Conn*, char*, int); 324 | int writepkt(Conn*, char*, int); 325 | int fmtpkt(Conn*, char*, ...); 326 | int flushpkt(Conn*); 327 | void initconn(Conn*, int, int); 328 | int gitconnect(Conn *, char *, char *); 329 | int readphase(Conn *); 330 | int writephase(Conn *); 331 | void closeconn(Conn *); 332 | 333 | /* queues */ 334 | void qinit(Objq*); 335 | void qclear(Objq*); 336 | void qput(Objq*, Object*, int); 337 | int qpop(Objq*, Qelt*); 338 | -------------------------------------------------------------------------------- /gitfs.4.man: -------------------------------------------------------------------------------- 1 | .TH GITFS 4 2 | .SH NAME 3 | git/fs \- git file server 4 | 5 | .SH SYNOPSIS 6 | 7 | git/fs 8 | [ 9 | .B -d 10 | ] 11 | [ 12 | .B -m 13 | .I mtpt 14 | ] 15 | 16 | .SH DESCRIPTION 17 | 18 | .PP 19 | Git/fs serves a file system interface to a git repository in the 20 | current directory. 21 | This file system provides a read-only view into the repository contents. 22 | By default, it is mounted on 23 | .B $repo/.git/fs. 24 | It does not cache mutable data, so any changes to the git repository will immediately be reflected in git/fs. 25 | 26 | .PP 27 | Git/fs serves a few levels of hierarchy. 28 | The top level contains the following files and directories: 29 | 30 | .TP 31 | .B branch 32 | Exposes branches. Branches are aliases for commit objects. 33 | 34 | .TP 35 | .B object 36 | Exposes all objects in the git repository. 37 | Objects may be commits, trees, or blobs. 38 | 39 | .TP 40 | .B HEAD 41 | This is an alias for the current commit. 42 | 43 | .PP 44 | Commits are also represented as small hierarchies. They contain 45 | the following files: 46 | 47 | .TP 48 | .B author 49 | This is the author of the commit. 50 | The contents of this file are free-form text, but conventionally 51 | they take the form 52 | .B Full Name 53 | 54 | .TP 55 | .B hash 56 | The commit id of the current branch 57 | 58 | .TP 59 | .B msg 60 | The full text of the commit message. 61 | 62 | .TP 63 | .B parent 64 | The list of parent commit ids of the current commit. 65 | One parent is listed per line. 66 | 67 | .TP 68 | .B tree 69 | A directory containing the tree associated with the 70 | commit. 71 | The timestamp of the files contained within this 72 | hierarchy are the same as the date of the commit. 73 | 74 | .PP 75 | Trees are presented as directory listings, and blobs 76 | as files. 77 | 78 | .SH FILES 79 | .TP 80 | .B .git 81 | The git repository being expected. 82 | .TP 83 | .B .git/HEAD 84 | A reference to the current HEAD. 85 | Used to populate 86 | .B $repo/.git/fs/HEAD 87 | .TP 88 | .git/config 89 | The per-repository configuation for git tools. 90 | .TP 91 | .B $home/lib/git/config 92 | The global configuration for git tools. 93 | 94 | .SH SOURCE 95 | .TP 96 | .B /sys/src/cmd/git/fs.c 97 | 98 | .SH "SEE ALSO" 99 | .IR git (1) 100 | .IR hg (1) 101 | .IR hgfs (4) 102 | 103 | .SH BUGS 104 | Symlinks are only partially supported. 105 | Symlinks are treated as regular files when reading. 106 | Modifying symlinks is unsupported. 107 | 108 | .PP 109 | There is no way to inspect the raw objects. This is 110 | a feature that would be useful for debugging. 111 | 112 | 113 | -------------------------------------------------------------------------------- /import: -------------------------------------------------------------------------------- 1 | #!/bin/rc 2 | rfork ne 3 | . /sys/lib/git/common.rc 4 | 5 | diffpath=/tmp/gitimport.$pid.diff 6 | fn sigexit { 7 | rm -f $diffpath 8 | } 9 | 10 | 11 | fn apply @{ 12 | git/fs 13 | amail='' 14 | aname='' 15 | msg='' 16 | whoami 17 | parents='-p'^`{git/query HEAD} 18 | branch=`{git/branch} 19 | if(test -e $gitfs/branch/$branch/tree) 20 | refpath=.git/refs/$branch 21 | if not if(test -e $gitfs/object/$branch/tree) 22 | refpath=.git/HEAD 23 | if not 24 | die 'invalid branch:' $branch 25 | awk ' 26 | BEGIN{ 27 | state="headers" 28 | } 29 | state=="headers" && /^From:/ { 30 | sub(/^From:[ \t]*/, "", $0); 31 | aname=$0; 32 | amail=$0; 33 | sub(/[ \t]*<.*$/, "", aname); 34 | sub(/^[^<]*[^>]*$/, "", amail); 36 | } 37 | state=="headers" && /^Date:/{ 38 | sub(/^Date:[ \t]*/, "", $0) 39 | date=$0 40 | } 41 | state=="headers" && /^Subject:/{ 42 | sub(/^Subject:[ \t]*(\[[^\]]*\][ \t]*)*/, "", $0); 43 | gotmsg = 1 44 | print > "/env/msg" 45 | } 46 | state=="headers" && /^$/ { 47 | state="body" 48 | } 49 | (state=="headers" || state=="body") && (/^diff / || /^---( |$)/){ 50 | state="diff" 51 | } 52 | state=="body" && /^[ ]*$/ { 53 | empty=1 54 | next 55 | } 56 | state=="body" { 57 | if(empty) 58 | printf "\n" > "/env/msg" 59 | empty=0 60 | sub(/[ ]+$/, "") 61 | print > "/env/msg" 62 | } 63 | state=="diff" { 64 | print > ENVIRON["diffpath"] 65 | } 66 | END{ 67 | if(state != "diff") 68 | exit("malformed patch: " state); 69 | if(aname == "" || amail == "" || date == "" || gotmsg == "") 70 | exit("missing headers"); 71 | printf "%s", aname > "/env/aname" 72 | printf "%s", amail > "/env/amail" 73 | printf "%s", date > "/env/date" 74 | } 75 | ' || die 'could not import:' $status 76 | 77 | # force re-reading env 78 | rc -c ' 79 | date=`{seconds $date} 80 | files=`$nl{patch -np1 < $diffpath} 81 | if(! git/walk -q $files){ 82 | >[1=2] { 83 | echo patch would clobber files: 84 | git/walk $files 85 | exit clobber 86 | } 87 | } 88 | echo applying $msg | sed 1q 89 | if(! files=`$nl{patch -p1 < $diffpath}) 90 | die ''patch failed'' 91 | for(f in $files){ 92 | if(test -e $f) 93 | git/add $f 94 | if not 95 | git/add -r $f 96 | } 97 | git/walk -fRMA $files 98 | if(~ $#nocommit 0){ 99 | if(hash=`{git/save -n $aname -e $amail -N $name -E $email -m $msg -d $date $parents $files}){ 100 | echo $hash > $refpath 101 | for(f in $files) 102 | echo T NOQID 0 $f >> .git/INDEX9 103 | } 104 | } 105 | status='''' 106 | ' 107 | } 108 | 109 | gitup 110 | 111 | flagfmt='n:nocommit'; args='file ...' 112 | eval `''{aux/getflags $*} || exec aux/usage 113 | 114 | patches=(/fd/0) 115 | if(! ~ $#* 0) 116 | patches=`{cleanname -d $gitrel $*} 117 | for(p in $patches){ 118 | # upas serves the decoded header and body separately, 119 | # so we cat them together when applying a upas message. 120 | # 121 | # this allows mime-encoded or line-wrapped patches. 122 | if(test -d $p && test -f $p/header && test -f $p/body) 123 | {{cat $p/header; echo; cat $p/body} | apply} || die $status 124 | if not 125 | apply < $p 126 | } 127 | exit '' 128 | -------------------------------------------------------------------------------- /init: -------------------------------------------------------------------------------- 1 | #!/bin/rc -e 2 | rfork ne 3 | . /sys/lib/git/common.rc 4 | 5 | flagfmt='u:upstream upstream,b:branch branch'; args='name' 6 | eval `''{aux/getflags $*} || exec aux/usage 7 | 8 | dir=$1 9 | if(~ $#dir 0) 10 | dir=. 11 | if(~ $#branch 0) 12 | branch=front 13 | if(test -e $dir/.git) 14 | die $dir/.git already exists 15 | name=`{basename `{cleanname -d `{pwd} $dir}} 16 | if(~ $#upstream 0){ 17 | upstream=`{git/conf 'defaults "origin".baseurl'} 18 | if(! ~ $#upstream 0) 19 | upstream=$upstream/$name 20 | } 21 | 22 | mkdir -p $dir/.git/refs/^(heads remotes) 23 | mkdir -p $dir/.git/^(fs objects) 24 | >$dir/.git/config { 25 | echo '[core]' 26 | echo ' repositoryformatversion = p9.0' 27 | if(! ~ $#upstream 0){ 28 | echo '[remote "origin"]' 29 | echo ' url = '$upstream 30 | } 31 | echo '[branch "'$branch'"]' 32 | echo ' remote = origin' 33 | } 34 | >$dir/.git/INDEX9 35 | >$dir/.git/HEAD { 36 | echo ref: refs/heads/$branch 37 | } 38 | 39 | exit '' 40 | -------------------------------------------------------------------------------- /log: -------------------------------------------------------------------------------- 1 | #!/bin/rc -e 2 | rfork en 3 | . /sys/lib/git/common.rc 4 | 5 | gitup 6 | 7 | flagfmt='e:expr expression, c:branch commit, s:short'; args='file ...' 8 | eval `''{aux/getflags $*} || exec aux/usage 9 | 10 | base=/mnt/git/object/ 11 | if(~ $#branch 0) 12 | branch=`{git/branch} 13 | if(~ $#expr 0) 14 | commits=`{git/query $branch} 15 | if not 16 | commits=`{git/query $expr} 17 | 18 | files=() 19 | if(! ~ $#* 0) 20 | files=`"{walk -f $gitrel/^$* | subst '^\./' | sort} 21 | 22 | while(! ~ $#commits 0){ 23 | ids=$nids 24 | show=() 25 | c=$commits(1) 26 | if(! ~ $#files 0){ 27 | ncomm=`{comm -12 /env/files <{git/query -c $c~ $c | subst '^..' | sort} | wc -l} 28 | if(! ~ $ncomm 0) 29 | show=true 30 | } 31 | commits=$commits(2-) 32 | if(~ $#expr 0) 33 | commits=($commits `{cat $base/$c/parent >[2]/dev/null}) 34 | if(! ~ $#commits 0) 35 | commits=`$nl{walk -emp -n0 $base^$commits | sort -rn | uniq | awk -F/ '{print $NF}'} 36 | 37 | if(~ $#files 0 || ~ $show true){ 38 | if(~ $short 1) 39 | echo $c `{cat $base/$c/msg | sed 1q} 40 | if not{ 41 | echo -n 'Hash: '`''{cat $base/$c/hash} 42 | echo -n 'Author: '`''{cat $base/$c/author} 43 | echo -n 'Date: '`''{date `{mtime $base/$c/msg | awk '{print $1}'}} 44 | subst -g '^' ' ' <$base/$c/msg 45 | echo 46 | } 47 | } 48 | } 49 | exit '' 50 | -------------------------------------------------------------------------------- /log.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "git.h" 4 | 5 | typedef struct Pfilt Pfilt; 6 | struct Pfilt { 7 | char *elt; 8 | int show; 9 | Pfilt *sub; 10 | int nsub; 11 | }; 12 | 13 | Biobuf *out; 14 | char *queryexpr; 15 | char *commitid; 16 | int shortlog; 17 | int msgcount = -1; 18 | 19 | Objset done; 20 | Objq objq; 21 | Pfilt *pathfilt; 22 | 23 | void 24 | filteradd(Pfilt *pf, char *path) 25 | { 26 | char *p, *e; 27 | int i; 28 | 29 | if((e = strchr(path, '/')) != nil) 30 | p = smprint("%.*s", (int)(e - path), path); 31 | else 32 | p = strdup(path); 33 | 34 | while(e != nil && *e == '/') 35 | e++; 36 | for(i = 0; i < pf->nsub; i++){ 37 | if(strcmp(pf->sub[i].elt, p) == 0){ 38 | pf->sub[i].show = pf->sub[i].show || (e == nil); 39 | if(e != nil) 40 | filteradd(&pf->sub[i], e); 41 | free(p); 42 | return; 43 | } 44 | } 45 | pf->sub = earealloc(pf->sub, pf->nsub+1, sizeof(Pfilt)); 46 | pf->sub[pf->nsub].elt = p; 47 | pf->sub[pf->nsub].show = (e == nil); 48 | pf->sub[pf->nsub].nsub = 0; 49 | pf->sub[pf->nsub].sub = nil; 50 | if(e != nil) 51 | filteradd(&pf->sub[pf->nsub], e); 52 | pf->nsub++; 53 | } 54 | 55 | Hash 56 | lookup(Pfilt *pf, Object *o) 57 | { 58 | int i; 59 | 60 | for(i = 0; i < o->tree->nent; i++) 61 | if(strcmp(o->tree->ent[i].name, pf->elt) == 0) 62 | return o->tree->ent[i].h; 63 | return Zhash; 64 | } 65 | 66 | int 67 | matchesfilter1(Pfilt *pf, Object *t, Object *pt) 68 | { 69 | Object *a, *b; 70 | Hash ha, hb; 71 | int i, r; 72 | 73 | if(pf->show) 74 | return 1; 75 | if(t->type != pt->type) 76 | return 1; 77 | if(t->type != GTree) 78 | return 0; 79 | 80 | for(i = 0; i < pf->nsub; i++){ 81 | ha = lookup(&pf->sub[i], t); 82 | hb = lookup(&pf->sub[i], pt); 83 | if(hasheq(&ha, &hb)) 84 | continue; 85 | if(hasheq(&ha, &Zhash) || hasheq(&hb, &Zhash)) 86 | return 1; 87 | if((a = readobject(ha)) == nil) 88 | sysfatal("read %H: %r", ha); 89 | if((b = readobject(hb)) == nil) 90 | sysfatal("read %H: %r", hb); 91 | r = matchesfilter1(&pf->sub[i], a, b); 92 | unref(a); 93 | unref(b); 94 | if(r) 95 | return 1; 96 | } 97 | return 0; 98 | } 99 | 100 | int 101 | matchesfilter(Object *o) 102 | { 103 | Object *t, *p, *pt; 104 | int i, r; 105 | 106 | assert(o->type == GCommit); 107 | if(pathfilt == nil) 108 | return 1; 109 | if((t = readobject(o->commit->tree)) == nil) 110 | sysfatal("read %H: %r", o->commit->tree); 111 | for(i = 0; i < o->commit->nparent; i++){ 112 | if((p = readobject(o->commit->parent[i])) == nil) 113 | sysfatal("read %H: %r", o->commit->parent[i]); 114 | if((pt = readobject(p->commit->tree)) == nil) 115 | sysfatal("read %H: %r", o->commit->tree); 116 | r = matchesfilter1(pathfilt, t, pt); 117 | unref(p); 118 | unref(pt); 119 | if(r) 120 | return 1; 121 | } 122 | return o->commit->nparent == 0; 123 | } 124 | 125 | 126 | static char* 127 | nextline(char *p, char *e) 128 | { 129 | for(; p != e; p++) 130 | if(*p == '\n') 131 | break; 132 | return p; 133 | } 134 | 135 | static int 136 | show(Object *o) 137 | { 138 | Tm tm; 139 | char *p, *q, *e; 140 | 141 | assert(o->type == GCommit); 142 | if(shortlog){ 143 | p = o->commit->msg; 144 | e = p + o->commit->nmsg; 145 | q = nextline(p, e); 146 | Bprint(out, "%H ", o->hash); 147 | Bwrite(out, p, q - p); 148 | Bputc(out, '\n'); 149 | }else{ 150 | tmtime(&tm, o->commit->mtime, tzload("local")); 151 | Bprint(out, "Hash:\t%H\n", o->hash); 152 | Bprint(out, "Author:\t%s\n", o->commit->author); 153 | if(o->commit->committer != nil 154 | && strcmp(o->commit->author, o->commit->committer) != 0) 155 | Bprint(out, "Committer:\t%s\n", o->commit->committer); 156 | Bprint(out, "Date:\t%τ\n", tmfmt(&tm, "WW MMM D hh:mm:ss z YYYY")); 157 | Bprint(out, "\n"); 158 | p = o->commit->msg; 159 | e = p + o->commit->nmsg; 160 | for(; p != e; p = q){ 161 | q = nextline(p, e); 162 | Bputc(out, '\t'); 163 | Bwrite(out, p, q - p); 164 | Bputc(out, '\n'); 165 | if(q != e) 166 | q++; 167 | } 168 | Bprint(out, "\n"); 169 | } 170 | Bflush(out); 171 | return 1; 172 | } 173 | 174 | static void 175 | showquery(char *q) 176 | { 177 | Object *o; 178 | Hash *h; 179 | int n, i; 180 | 181 | if((n = resolverefs(&h, q)) == -1) 182 | sysfatal("resolve: %r"); 183 | for(i = 0; i < n && (msgcount == -1 || msgcount > 0); i++){ 184 | if((o = readobject(h[i])) == nil) 185 | sysfatal("read %H: %r", h[i]); 186 | if(matchesfilter(o)){ 187 | show(o); 188 | if(msgcount != -1) 189 | msgcount--; 190 | } 191 | unref(o); 192 | } 193 | exits(nil); 194 | } 195 | 196 | static void 197 | showcommits(char *c) 198 | { 199 | Object *o, *p; 200 | Qelt e; 201 | int i; 202 | Hash h; 203 | 204 | if(c == nil) 205 | c = "HEAD"; 206 | if(resolveref(&h, c) == -1) 207 | sysfatal("resolve %s: %r", c); 208 | if((o = readobject(h)) == nil) 209 | sysfatal("load %H: %r", h); 210 | if(o->type != GCommit) 211 | sysfatal("%s: not a commit", c); 212 | qinit(&objq); 213 | osinit(&done); 214 | qput(&objq, o, 0); 215 | while(qpop(&objq, &e) && (msgcount == -1 || msgcount > 0)){ 216 | if(matchesfilter(e.o)){ 217 | show(e.o); 218 | if(msgcount != -1) 219 | msgcount--; 220 | } 221 | for(i = 0; i < e.o->commit->nparent; i++){ 222 | if(oshas(&done, e.o->commit->parent[i])) 223 | continue; 224 | if((p = readobject(e.o->commit->parent[i])) == nil) 225 | sysfatal("load %H: %r", o->commit->parent[i]); 226 | osadd(&done, p); 227 | qput(&objq, p, 0); 228 | } 229 | unref(e.o); 230 | } 231 | } 232 | 233 | static void 234 | usage(void) 235 | { 236 | fprint(2, "usage: %s [-s] [-e expr | -c commit] files..\n", argv0); 237 | exits("usage"); 238 | } 239 | 240 | void 241 | main(int argc, char **argv) 242 | { 243 | char path[1024], repo[1024], *p, *r; 244 | int i, nrel, nrepo; 245 | 246 | ARGBEGIN{ 247 | case 'e': 248 | queryexpr = EARGF(usage()); 249 | break; 250 | case 'c': 251 | commitid = EARGF(usage()); 252 | break; 253 | case 's': 254 | shortlog++; 255 | break; 256 | case 'n': 257 | msgcount = atoi(EARGF(usage())); 258 | break; 259 | default: 260 | usage(); 261 | break; 262 | }ARGEND; 263 | 264 | if(findrepo(repo, sizeof(repo), &nrel) == -1) 265 | sysfatal("find root: %r"); 266 | nrepo = strlen(repo); 267 | if(argc != 0){ 268 | if(getwd(path, sizeof(path)) == nil) 269 | sysfatal("getwd: %r"); 270 | if(strncmp(path, repo, nrepo) != 0) 271 | sysfatal("path shifted??"); 272 | p = path + nrepo; 273 | pathfilt = emalloc(sizeof(Pfilt)); 274 | for(i = 0; i < argc; i++){ 275 | if(*argv[i] == '/'){ 276 | if(strncmp(argv[i], repo, nrepo) != 0) 277 | continue; 278 | r = smprint("./%s", argv[i]+nrepo); 279 | }else 280 | r = smprint("./%s/%s", p, argv[i]); 281 | cleanname(r); 282 | filteradd(pathfilt, r); 283 | free(r); 284 | } 285 | } 286 | if(chdir(repo) == -1) 287 | sysfatal("chdir: %r"); 288 | 289 | gitinit(); 290 | tmfmtinstall(); 291 | out = Bfdopen(1, OWRITE); 292 | if(queryexpr != nil) 293 | showquery(queryexpr); 294 | else 295 | showcommits(commitid); 296 | Bterm(out); 297 | exits(nil); 298 | } 299 | -------------------------------------------------------------------------------- /merge: -------------------------------------------------------------------------------- 1 | #!/bin/rc -e 2 | rfork ne 3 | . /sys/lib/git/common.rc 4 | 5 | fn merge{ 6 | ourbr=$gitfs/object/$1/tree 7 | basebr=$gitfs/object/$2/tree 8 | theirbr=$gitfs/object/$3/tree 9 | 10 | all=`$nl{{git/query -c $1 $2; git/query -c $2 $3} | sed 's/^..//' | sort | uniq} 11 | for(f in $all){ 12 | ours=$ourbr/$f 13 | base=$basebr/$f 14 | theirs=$theirbr/$f 15 | merge1 ./$f $ours $base $theirs 16 | } 17 | } 18 | 19 | gitup 20 | 21 | flagfmt=''; args='theirs' 22 | eval `''{aux/getflags $*} || exec aux/usage 23 | 24 | if(! ~ $#* 1) 25 | exec aux/usage 26 | 27 | theirs=`{git/query $1} 28 | ours=`{git/query HEAD} 29 | base=`{git/query $theirs ^ ' ' ^ $ours ^ '@'} 30 | 31 | if(~ $base $theirs) 32 | die 'nothing to merge, doofus' 33 | if(! git/walk -q) 34 | die 'dirty work tree, refusing to merge' 35 | if(~ $base $ours){ 36 | >[1=2] echo 'fast forwarding...' 37 | echo $theirs > .git/refs/`{git/branch} 38 | git/revert . 39 | exit '' 40 | } 41 | echo $ours >> .git/merge-parents 42 | echo $theirs >> .git/merge-parents 43 | 44 | merge $ours $base $theirs 45 | >[1=2] echo 'merge complete: remember to commit' 46 | exit '' 47 | -------------------------------------------------------------------------------- /mkfile: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | #include "git.h" 5 | 6 | void 7 | osinit(Objset *s) 8 | { 9 | s->sz = 16; 10 | s->nobj = 0; 11 | s->obj = eamalloc(s->sz, sizeof(Hash)); 12 | } 13 | 14 | void 15 | osclear(Objset *s) 16 | { 17 | free(s->obj); 18 | } 19 | 20 | void 21 | osadd(Objset *s, Object *o) 22 | { 23 | u32int probe; 24 | Object **obj; 25 | int i, sz; 26 | 27 | probe = GETBE32(o->hash.h) % s->sz; 28 | while(s->obj[probe]){ 29 | if(hasheq(&s->obj[probe]->hash, &o->hash)){ 30 | s->obj[probe] = o; 31 | return; 32 | } 33 | probe = (probe + 1) % s->sz; 34 | } 35 | assert(s->obj[probe] == nil); 36 | s->obj[probe] = o; 37 | s->nobj++; 38 | if(s->sz < 2*s->nobj){ 39 | sz = s->sz; 40 | obj = s->obj; 41 | 42 | s->sz *= 2; 43 | s->nobj = 0; 44 | s->obj = eamalloc(s->sz, sizeof(Hash)); 45 | for(i = 0; i < sz; i++) 46 | if(obj[i]) 47 | osadd(s, obj[i]); 48 | free(obj); 49 | } 50 | } 51 | 52 | Object* 53 | osfind(Objset *s, Hash h) 54 | { 55 | u32int probe; 56 | 57 | for(probe = GETBE32(h.h) % s->sz; s->obj[probe]; probe = (probe + 1) % s->sz) 58 | if(hasheq(&s->obj[probe]->hash, &h)) 59 | return s->obj[probe]; 60 | return 0; 61 | } 62 | 63 | int 64 | oshas(Objset *s, Hash h) 65 | { 66 | return osfind(s, h) != nil; 67 | } 68 | -------------------------------------------------------------------------------- /ols.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "git.h" 5 | 6 | enum { 7 | Sinit, 8 | Siter, 9 | }; 10 | 11 | static int 12 | crackidx(char *path, int *np) 13 | { 14 | int fd; 15 | char buf[4]; 16 | 17 | if((fd = open(path, OREAD)) == -1) 18 | return -1; 19 | if(seek(fd, 8 + 255*4, 0) == -1) 20 | return -1; 21 | if(readn(fd, buf, sizeof(buf)) != sizeof(buf)) 22 | return -1; 23 | *np = GETBE32(buf); 24 | return fd; 25 | } 26 | 27 | int 28 | isloosedir(char *s) 29 | { 30 | return strlen(s) == 2 && isxdigit(s[0]) && isxdigit(s[1]); 31 | } 32 | 33 | int 34 | endswith(char *n, char *s) 35 | { 36 | int nn, ns; 37 | 38 | nn = strlen(n); 39 | ns = strlen(s); 40 | return nn > ns && strcmp(n + nn - ns, s) == 0; 41 | } 42 | 43 | int 44 | olsreadpacked(Objlist *ols, Hash *h) 45 | { 46 | char *p; 47 | int i, j; 48 | 49 | i = ols->packidx; 50 | j = ols->entidx; 51 | 52 | if(ols->state == Siter) 53 | goto step; 54 | for(i = 0; i < ols->npack; i++){ 55 | if(!endswith(ols->pack[i].name, ".idx")) 56 | continue; 57 | if((p = smprint(".git/objects/pack/%s", ols->pack[i].name)) == nil) 58 | sysfatal("smprint: %r"); 59 | ols->fd = crackidx(p, &ols->nent); 60 | free(p); 61 | if(ols->fd == -1) 62 | continue; 63 | j = 0; 64 | while(j < ols->nent){ 65 | if(readn(ols->fd, h->h, sizeof(h->h)) != sizeof(h->h)) 66 | continue; 67 | ols->state = Siter; 68 | ols->packidx = i; 69 | ols->entidx = j; 70 | return 0; 71 | step: 72 | j++; 73 | } 74 | close(ols->fd); 75 | } 76 | ols->state = Sinit; 77 | return -1; 78 | } 79 | 80 | 81 | int 82 | olsreadloose(Objlist *ols, Hash *h) 83 | { 84 | char buf[64], *p; 85 | int i, j, n; 86 | 87 | i = ols->topidx; 88 | j = ols->looseidx; 89 | if(ols->state == Siter) 90 | goto step; 91 | for(i = 0; i < ols->ntop; i++){ 92 | if(!isloosedir(ols->top[i].name)) 93 | continue; 94 | if((p = smprint(".git/objects/%s", ols->top[i].name)) == nil) 95 | sysfatal("smprint: %r"); 96 | ols->fd = open(p, OREAD); 97 | free(p); 98 | if(ols->fd == -1) 99 | continue; 100 | while((ols->nloose = dirread(ols->fd, &ols->loose)) > 0){ 101 | j = 0; 102 | while(j < ols->nloose){ 103 | n = snprint(buf, sizeof(buf), "%s%s", ols->top[i].name, ols->loose[j].name); 104 | if(n >= sizeof(buf)) 105 | goto step; 106 | if(hparse(h, buf) == -1) 107 | goto step; 108 | ols->state = Siter; 109 | ols->topidx = i; 110 | ols->looseidx = j; 111 | return 0; 112 | step: 113 | j++; 114 | } 115 | free(ols->loose); 116 | ols->loose = nil; 117 | } 118 | close(ols->fd); 119 | ols->fd = -1; 120 | } 121 | ols->state = Sinit; 122 | return -1; 123 | } 124 | 125 | Objlist* 126 | mkols(void) 127 | { 128 | Objlist *ols; 129 | 130 | ols = emalloc(sizeof(Objlist)); 131 | if((ols->ntop = slurpdir(".git/objects", &ols->top)) == -1) 132 | sysfatal("read top level: %r"); 133 | if((ols->npack = slurpdir(".git/objects/pack", &ols->pack)) == -1) 134 | ols->pack = nil; 135 | ols->fd = -1; 136 | return ols; 137 | } 138 | 139 | void 140 | olsfree(Objlist *ols) 141 | { 142 | if(ols == nil) 143 | return; 144 | if(ols->fd != -1) 145 | close(ols->fd); 146 | free(ols->top); 147 | free(ols->loose); 148 | free(ols->pack); 149 | free(ols); 150 | } 151 | 152 | int 153 | olsnext(Objlist *ols, Hash *h) 154 | { 155 | if(ols->stage == 0){ 156 | if(olsreadloose(ols, h) != -1){ 157 | ols->idx++; 158 | return 0; 159 | } 160 | ols->stage++; 161 | } 162 | if(ols->stage == 1){ 163 | if(olsreadpacked(ols, h) != -1){ 164 | ols->idx++; 165 | return 0; 166 | } 167 | ols->stage++; 168 | } 169 | return -1; 170 | } 171 | -------------------------------------------------------------------------------- /proto.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "git.h" 6 | 7 | #define Useragent "useragent git/2.24.1" 8 | #define Contenthdr "headers Content-Type: application/x-git-%s-pack-request" 9 | #define Accepthdr "headers Accept: application/x-git-%s-pack-result" 10 | 11 | enum { 12 | Nproto = 16, 13 | Nport = 16, 14 | Nhost = 256, 15 | Npath = 128, 16 | Nrepo = 64, 17 | Nbranch = 32, 18 | }; 19 | 20 | void 21 | tracepkt(int v, char *pfx, char *b, int n) 22 | { 23 | char *f; 24 | int o, i; 25 | 26 | if(chattygit < v) 27 | return; 28 | o = 0; 29 | f = emalloc(n*4 + 1); 30 | for(i = 0; i < n; i++){ 31 | if(isprint(b[i])){ 32 | f[o++] = b[i]; 33 | continue; 34 | } 35 | f[o++] = '\\'; 36 | switch(b[i]){ 37 | case '\\': f[o++] = '\\'; break; 38 | case '\n': f[o++] = 'n'; break; 39 | case '\r': f[o++] = 'r'; break; 40 | case '\v': f[o++] = 'v'; break; 41 | case '\0': f[o++] = '0'; break; 42 | default: 43 | f[o++] = 'x'; 44 | f[o++] = "0123456789abcdef"[(b[i]>>4)&0xf]; 45 | f[o++] = "0123456789abcdef"[(b[i]>>0)&0xf]; 46 | break; 47 | } 48 | } 49 | f[o] = '\0'; 50 | fprint(2, "%s %04x:\t%s\n", pfx, n, f); 51 | free(f); 52 | } 53 | 54 | int 55 | readpkt(Conn *c, char *buf, int nbuf) 56 | { 57 | char len[5]; 58 | char *e; 59 | int n; 60 | 61 | if(readn(c->rfd, len, 4) != 4){ 62 | werrstr("pktline: short read from transport"); 63 | return -1; 64 | } 65 | len[4] = 0; 66 | n = strtol(len, &e, 16); 67 | if(n == 0){ 68 | dprint(1, "=r=> 0000\n"); 69 | return 0; 70 | } 71 | if(e != len + 4 || n <= 4) 72 | sysfatal("pktline: bad length '%s'", len); 73 | n -= 4; 74 | if(n >= nbuf) 75 | sysfatal("pktline: undersize buffer"); 76 | if(readn(c->rfd, buf, n) != n) 77 | return -1; 78 | if(n > 4 && strncmp(buf, "ERR ", 4) == 0){ 79 | if((e = strrchr(buf, '\n')) != nil) 80 | *e = '\0'; 81 | werrstr("%s", buf + 4); 82 | return -1; 83 | } 84 | buf[n] = 0; 85 | tracepkt(1, "=r=>", buf, n); 86 | return n; 87 | } 88 | 89 | int 90 | writepkt(Conn *c, char *buf, int nbuf) 91 | { 92 | char len[5]; 93 | 94 | 95 | snprint(len, sizeof(len), "%04x", nbuf + 4); 96 | if(write(c->wfd, len, 4) != 4) 97 | return -1; 98 | if(write(c->wfd, buf, nbuf) != nbuf) 99 | return -1; 100 | tracepkt(1, "<=w=", buf, nbuf); 101 | return 0; 102 | } 103 | 104 | int 105 | fmtpkt(Conn *c, char *fmt, ...) 106 | { 107 | char pkt[Pktmax]; 108 | va_list ap; 109 | int n; 110 | 111 | va_start(ap, fmt); 112 | n = vsnprint(pkt, sizeof(pkt), fmt, ap); 113 | n = writepkt(c, pkt, n); 114 | va_end(ap); 115 | return n; 116 | } 117 | 118 | int 119 | flushpkt(Conn *c) 120 | { 121 | dprint(1, "<=w= 0000\n"); 122 | return write(c->wfd, "0000", 4); 123 | } 124 | 125 | static void 126 | grab(char *dst, int n, char *p, char *e) 127 | { 128 | int l; 129 | 130 | l = e - p; 131 | if(l >= n) 132 | sysfatal("overlong component"); 133 | memcpy(dst, p, l); 134 | dst[l] = 0; 135 | } 136 | 137 | static int 138 | parseuri(char *uri, char *proto, char *host, char *port, char *path, char *repo) 139 | { 140 | char *s, *p, *q; 141 | int n, hasport; 142 | print("uri: \"%s\"\n", uri); 143 | 144 | p = strstr(uri, "://"); 145 | if(p == nil) 146 | snprint(proto, Nproto, "ssh"); 147 | else if(strncmp(uri, "git+", 4) == 0) 148 | grab(proto, Nproto, uri + 4, p); 149 | else 150 | grab(proto, Nproto, uri, p); 151 | *port = 0; 152 | hasport = 1; 153 | if(strcmp(proto, "git") == 0) 154 | snprint(port, Nport, "9418"); 155 | else if(strncmp(proto, "https", 5) == 0) 156 | snprint(port, Nport, "443"); 157 | else if(strncmp(proto, "http", 4) == 0) 158 | snprint(port, Nport, "80"); 159 | else if(strncmp(proto, "hjgit", 5) == 0) 160 | snprint(port, Nport, "17021"); 161 | else if(strncmp(proto, "gits", 5) == 0) 162 | snprint(port, Nport, "9419"); 163 | else 164 | hasport = 0; 165 | s = (p != nil) ? p + 3 : uri; 166 | p = nil; 167 | if(!hasport){ 168 | p = strstr(s, ":"); 169 | if(p != nil) 170 | p++; 171 | } 172 | if(p == nil) 173 | p = strstr(s, "/"); 174 | if(p == nil || strlen(p) == 1){ 175 | werrstr("missing path"); 176 | return -1; 177 | } 178 | 179 | q = memchr(s, ':', p - s); 180 | if(q){ 181 | grab(host, Nhost, s, q); 182 | grab(port, Nport, q + 1, p); 183 | }else{ 184 | grab(host, Nhost, s, p); 185 | } 186 | 187 | snprint(path, Npath, "%s", p); 188 | if((q = strrchr(p, '/')) != nil) 189 | p = q + 1; 190 | if(strlen(p) == 0){ 191 | werrstr("missing repository in uri"); 192 | return -1; 193 | } 194 | n = strlen(p); 195 | if(hassuffix(p, ".git")) 196 | n -= 4; 197 | grab(repo, Nrepo, p, p + n); 198 | return 0; 199 | } 200 | 201 | static int 202 | webclone(Conn *c, char *url) 203 | { 204 | char buf[16]; 205 | int n, conn; 206 | 207 | if((c->cfd = open("/mnt/web/clone", ORDWR)) < 0) 208 | goto err; 209 | if((n = read(c->cfd, buf, sizeof(buf)-1)) == -1) 210 | goto err; 211 | buf[n] = 0; 212 | conn = atoi(buf); 213 | 214 | /* github will behave differently based on useragent */ 215 | if(write(c->cfd, Useragent, sizeof(Useragent)) == -1) 216 | return -1; 217 | dprint(1, "open url %s\n", url); 218 | if(fprint(c->cfd, "url %s", url) == -1) 219 | goto err; 220 | free(c->dir); 221 | c->dir = smprint("/mnt/web/%d", conn); 222 | return 0; 223 | err: 224 | if(c->cfd != -1) 225 | close(c->cfd); 226 | return -1; 227 | } 228 | 229 | static int 230 | webopen(Conn *c, char *file, int mode) 231 | { 232 | char path[128]; 233 | int fd; 234 | 235 | snprint(path, sizeof(path), "%s/%s", c->dir, file); 236 | if((fd = open(path, mode)) == -1) 237 | return -1; 238 | return fd; 239 | } 240 | 241 | static int 242 | issmarthttp(Conn *c, char *direction) 243 | { 244 | char buf[Pktmax+1], svc[128]; 245 | int fd, n; 246 | 247 | if((fd = webopen(c, "contenttype", OREAD)) == -1) 248 | return -1; 249 | n = readn(fd, buf, sizeof(buf) - 1); 250 | close(fd); 251 | if(n == -1) 252 | return -1; 253 | buf[n] = '\0'; 254 | snprint(svc, sizeof(svc), "application/x-git-%s-pack-advertisement", direction); 255 | if(strcmp(svc, buf) != 0){ 256 | werrstr("dumb http protocol not supported"); 257 | return -1; 258 | } 259 | 260 | if((n = readpkt(c, buf, sizeof(buf))) == -1) 261 | sysfatal("http read: %r"); 262 | buf[n] = 0; 263 | snprint(svc, sizeof(svc), "# service=git-%s-pack\n", direction); 264 | if(strncmp(svc, buf, n) != 0){ 265 | werrstr("invalid initial packet line"); 266 | return -1; 267 | } 268 | if(readpkt(c, buf, sizeof(buf)) != 0){ 269 | werrstr("protocol garble: expected flushpkt"); 270 | return -1; 271 | } 272 | return 0; 273 | } 274 | 275 | static int 276 | dialhttp(Conn *c, char *host, char *port, char *path, char *direction) 277 | { 278 | char *geturl, *suff, *hsep, *psep; 279 | 280 | suff = ""; 281 | hsep = ""; 282 | psep = ""; 283 | if(port && strlen(port) != 0) 284 | hsep = ":"; 285 | if(path && path[0] != '/') 286 | psep = "/"; 287 | memset(c, 0, sizeof(*c)); 288 | geturl = smprint("https://%s%s%s%s%s%s/info/refs?service=git-%s-pack", host, hsep, port, psep, path, suff, direction); 289 | c->type = ConnHttp; 290 | c->url = smprint("https://%s%s%s%s%s%s/git-%s-pack", host, hsep, port, psep, path, suff, direction); 291 | c->cfd = webclone(c, geturl); 292 | free(geturl); 293 | if(c->cfd == -1) 294 | return -1; 295 | c->rfd = webopen(c, "body", OREAD); 296 | c->wfd = -1; 297 | if(c->rfd == -1) 298 | return -1; 299 | if(issmarthttp(c, direction) == -1) 300 | return -1; 301 | c->direction = estrdup(direction); 302 | return 0; 303 | } 304 | 305 | static int 306 | dialssh(Conn *c, char *host, char *, char *path, char *direction) 307 | { 308 | int pid, pfd[2]; 309 | char cmd[64]; 310 | 311 | if(pipe(pfd) == -1) 312 | sysfatal("unable to open pipe: %r"); 313 | pid = fork(); 314 | if(pid == -1) 315 | sysfatal("unable to fork"); 316 | if(pid == 0){ 317 | close(pfd[1]); 318 | dup(pfd[0], 0); 319 | dup(pfd[0], 1); 320 | snprint(cmd, sizeof(cmd), "git-%s-pack", direction); 321 | dprint(1, "exec ssh '%s' '%s' %s\n", host, cmd, path); 322 | execl("/bin/ssh", "ssh", host, cmd, path, nil); 323 | sysfatal("exec: %r"); 324 | } 325 | close(pfd[0]); 326 | c->type = ConnSsh; 327 | c->rfd = pfd[1]; 328 | c->wfd = dup(pfd[1], -1); 329 | return 0; 330 | } 331 | 332 | static int 333 | githandshake(Conn *c, char *host, char *path, char *direction) 334 | { 335 | char *p, *e, cmd[512]; 336 | 337 | p = cmd; 338 | e = cmd + sizeof(cmd); 339 | p = seprint(p, e - 1, "git-%s-pack %s", direction, path); 340 | if(host != nil) 341 | p = seprint(p + 1, e, "host=%s", host); 342 | if(writepkt(c, cmd, p - cmd + 1) == -1){ 343 | fprint(2, "failed to write message\n"); 344 | closeconn(c); 345 | return -1; 346 | } 347 | return 0; 348 | } 349 | 350 | static int 351 | dialhjgit(Conn *c, char *host, char *port, char *path, char *direction, int auth) 352 | { 353 | char *ds; 354 | int pid, pfd[2]; 355 | 356 | if((ds = netmkaddr(host, "tcp", port)) == nil) 357 | return -1; 358 | if(pipe(pfd) == -1) 359 | sysfatal("unable to open pipe: %r"); 360 | pid = fork(); 361 | if(pid == -1) 362 | sysfatal("unable to fork"); 363 | if(pid == 0){ 364 | close(pfd[1]); 365 | dup(pfd[0], 0); 366 | dup(pfd[0], 1); 367 | dprint(1, "exec tlsclient -a %s\n", ds); 368 | if(auth) 369 | execl("/bin/tlsclient", "tlsclient", "-a", ds, nil); 370 | else 371 | execl("/bin/tlsclient", "tlsclient", ds, nil); 372 | sysfatal("exec: %r"); 373 | } 374 | close(pfd[0]); 375 | c->type = ConnGit9; 376 | c->rfd = pfd[1]; 377 | c->wfd = dup(pfd[1], -1); 378 | return githandshake(c, host, path, direction); 379 | } 380 | 381 | void 382 | initconn(Conn *c, int rd, int wr) 383 | { 384 | c->type = ConnGit; 385 | c->rfd = rd; 386 | c->wfd = wr; 387 | } 388 | 389 | static int 390 | dialgit(Conn *c, char *host, char *port, char *path, char *direction) 391 | { 392 | char *ds; 393 | int fd; 394 | 395 | if((ds = netmkaddr(host, "tcp", port)) == nil) 396 | return -1; 397 | dprint(1, "dial %s git-%s-pack %s\n", ds, direction, path); 398 | fd = dial(ds, nil, nil, nil); 399 | if(fd == -1) 400 | return -1; 401 | c->type = ConnGit; 402 | c->rfd = fd; 403 | c->wfd = dup(fd, -1); 404 | return githandshake(c, host, path, direction); 405 | } 406 | 407 | static int 408 | servelocal(Conn *c, char *path, char *direction) 409 | { 410 | int pid, pfd[2]; 411 | 412 | if(pipe(pfd) == -1) 413 | sysfatal("unable to open pipe: %r"); 414 | pid = fork(); 415 | if(pid == -1) 416 | sysfatal("unable to fork"); 417 | if(pid == 0){ 418 | close(pfd[1]); 419 | dup(pfd[0], 0); 420 | dup(pfd[0], 1); 421 | execl("/bin/git/serve", "serve", "-w", nil); 422 | sysfatal("exec: %r"); 423 | } 424 | close(pfd[0]); 425 | c->type = ConnGit; 426 | c->rfd = pfd[1]; 427 | c->wfd = dup(pfd[1], -1); 428 | return githandshake(c, nil, path, direction); 429 | } 430 | 431 | static int 432 | localrepo(char *uri, char *path, int npath) 433 | { 434 | int fd; 435 | 436 | snprint(path, npath, "%s/.git/../", uri); 437 | fd = open(path, OREAD); 438 | if(fd < 0) 439 | return -1; 440 | if(fd2path(fd, path, npath) != 0){ 441 | close(fd); 442 | return -1; 443 | } 444 | close(fd); 445 | return 0; 446 | } 447 | 448 | int 449 | gitconnect(Conn *c, char *uri, char *direction) 450 | { 451 | char proto[Nproto], host[Nhost], port[Nport]; 452 | char repo[Nrepo], path[Npath]; 453 | 454 | memset(c, 0, sizeof(Conn)); 455 | c->rfd = c->wfd = c->cfd = -1; 456 | 457 | if(localrepo(uri, path, sizeof(path)) == 0) 458 | return servelocal(c, path, direction); 459 | 460 | if(parseuri(uri, proto, host, port, path, repo) == -1){ 461 | werrstr("bad uri %s", uri); 462 | return -1; 463 | } 464 | if(strcmp(proto, "ssh") == 0) 465 | return dialssh(c, host, port, path, direction); 466 | else if(strcmp(proto, "git") == 0) 467 | return dialgit(c, host, port, path, direction); 468 | else if(strcmp(proto, "hjgit") == 0) 469 | return dialhjgit(c, host, port, path, direction, 1); 470 | else if(strcmp(proto, "gits") == 0) 471 | return dialhjgit(c, host, port, path, direction, 0); 472 | else if(strcmp(proto, "http") == 0 || strcmp(proto, "https") == 0) 473 | return dialhttp(c, host, port, path, direction); 474 | werrstr("unknown protocol %s", proto); 475 | return -1; 476 | } 477 | 478 | int 479 | writephase(Conn *c) 480 | { 481 | char hdr[128]; 482 | int n; 483 | 484 | dprint(1, "start write phase\n"); 485 | if(c->type != ConnHttp) 486 | return 0; 487 | 488 | if(c->wfd != -1) 489 | close(c->wfd); 490 | if(c->cfd != -1) 491 | close(c->cfd); 492 | if((c->cfd = webclone(c, c->url)) == -1) 493 | return -1; 494 | n = snprint(hdr, sizeof(hdr), Contenthdr, c->direction); 495 | if(write(c->cfd, hdr, n) == -1) 496 | return -1; 497 | n = snprint(hdr, sizeof(hdr), Accepthdr, c->direction); 498 | if(write(c->cfd, hdr, n) == -1) 499 | return -1; 500 | if((c->wfd = webopen(c, "postbody", OWRITE)) == -1) 501 | return -1; 502 | c->rfd = -1; 503 | return 0; 504 | } 505 | 506 | int 507 | readphase(Conn *c) 508 | { 509 | dprint(1, "start read phase\n"); 510 | if(c->type != ConnHttp) 511 | return 0; 512 | if(close(c->wfd) == -1) 513 | return -1; 514 | if((c->rfd = webopen(c, "body", OREAD)) == -1) 515 | return -1; 516 | c->wfd = -1; 517 | return 0; 518 | } 519 | 520 | void 521 | closeconn(Conn *c) 522 | { 523 | close(c->rfd); 524 | close(c->wfd); 525 | switch(c->type){ 526 | case ConnGit: 527 | break; 528 | case ConnGit9: 529 | case ConnSsh: 530 | free(wait()); 531 | break; 532 | case ConnHttp: 533 | close(c->cfd); 534 | break; 535 | } 536 | } 537 | -------------------------------------------------------------------------------- /pull: -------------------------------------------------------------------------------- 1 | #!/bin/rc -e 2 | rfork en 3 | . /sys/lib/git/common.rc 4 | 5 | fn update{ 6 | upstream=$1 7 | url=$2 8 | dir=$3 9 | dflag=() 10 | if(! ~ $#debug 0) 11 | dflag='-d' 12 | {git/get $dflag -u $upstream $url >[2=3] || die $status} | awk ' 13 | /^remote/{ 14 | if($2=="HEAD") 15 | next 16 | ref=$2 17 | hash=$3 18 | gsub("^refs/heads", "refs/remotes/'$upstream'", ref) 19 | outfile = ".git/"ref 20 | system("mkdir -p `{basename -d "outfile"}"); 21 | print hash > outfile; 22 | close(outfile); 23 | } 24 | ' |[3] tr '\x0d' '\x0a' 25 | } 26 | 27 | gitup 28 | 29 | flagfmt='d:debug, q:quiet, f:fetchonly, 30 | u:upstream upstream' 31 | args='' 32 | eval `''{aux/getflags $*} || exec aux/usage 33 | 34 | if(~ $#upstream 0) 35 | upstream=origin 36 | remote=`$nl{git/conf 'remote "'$upstream'".url'} 37 | if(~ $#remote 0){ 38 | remote=$upstream 39 | upstream=THEM 40 | } 41 | 42 | update $upstream $remote 43 | if (~ $fetchonly 1) 44 | exit 45 | 46 | local=`{git/branch} 47 | remote=`{git/branch | subst '^(refs/)?heads' 'remotes/'$upstream} 48 | 49 | # we have local commits, but the remote hasn't changed. 50 | # in this case, we want to keep the local commits untouched. 51 | if(~ `{git/query HEAD $remote @} `{git/query $remote}){ 52 | echo 'up to date' >[1=2] 53 | exit 54 | } 55 | # The remote repository and our HEAD have diverged: we 56 | # need to merge. 57 | if(! ~ `{git/query HEAD $remote @} `{git/query HEAD}){ 58 | >[1=2]{ 59 | echo ours: `{git/query HEAD} 60 | echo theirs: `{git/query $remote} 61 | echo common: `{git/query HEAD $remote @} 62 | echo git/merge $remote 63 | } 64 | exit diverged 65 | } 66 | # The remote is directly ahead of the local, and we have 67 | # no local commits that need merging. 68 | if(~ $#quiet 0) 69 | git/log -s -e $local'..'$remote 70 | echo $remote':' `{git/query $local} '=>' `{git/query $remote} 71 | git/branch -mnb $remote $local 72 | exit '' 73 | -------------------------------------------------------------------------------- /push: -------------------------------------------------------------------------------- 1 | #!/bin/rc -e 2 | rfork en 3 | . /sys/lib/git/common.rc 4 | 5 | gitup 6 | 7 | flagfmt='a:pushall, b:branch branch, f:force, d:debug, 8 | r:remove remove, u:upstream upstream' args='' 9 | eval `''{aux/getflags $*} || exec aux/usage 10 | if(! ~ $#* 0) 11 | exec aux/usage 12 | 13 | if(~ $pushall 1) 14 | branch=`$nl{cd .git/refs/heads && walk -f} 15 | if(~ $#branch 0) 16 | branch=`{git/branch} 17 | if(~ $#branch 0) 18 | die 'no branches' 19 | if(~ $force 1) 20 | force=-f 21 | if(~ $debug 1) 22 | debug='-d' 23 | 24 | if(~ $#upstream 0) 25 | upstream=origin 26 | 27 | remotes=`$nl{git/conf -a 'remote "'$upstream'".url'} 28 | if(~ $#remotes 0) 29 | remotes=$upstream 30 | branch=-b^$branch 31 | if(! ~ $#remove 0) 32 | remove=-r^$remove 33 | for(remote in $remotes){ 34 | updates=`$nl{git/send $debug $force $branch $remove $remote} || die $status 35 | for(ln in $updates){ 36 | u=`{echo $ln} 37 | refpath=`{echo $u(2) | subst '^refs/heads/' '.git/refs/remotes/'$upstream'/'} 38 | switch($u(1)){ 39 | case update; 40 | mkdir -p `{basename -d $refpath} 41 | echo $u(4) > $refpath 42 | echo $u(2)^':' $u(3) '=>' $u(4) 43 | case delete; 44 | echo $u(2)^': removed' 45 | rm -f $refpath 46 | case uptodate; 47 | echo $u(2)^': up to date' 48 | } 49 | } 50 | } 51 | exit '' 52 | -------------------------------------------------------------------------------- /query.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "git.h" 5 | 6 | #pragma varargck type "P" void 7 | 8 | int fullpath; 9 | int changes; 10 | int reverse; 11 | char *path[128]; 12 | int npath; 13 | 14 | int 15 | Pfmt(Fmt *f) 16 | { 17 | int i, n; 18 | 19 | n = 0; 20 | for(i = 0; i < npath; i++) 21 | n += fmtprint(f, "%s/", path[i]); 22 | return n; 23 | } 24 | 25 | void 26 | showdir(Hash dh, char *dname, char m) 27 | { 28 | Dirent *p, *e; 29 | Object *d; 30 | 31 | 32 | path[npath++] = dname; 33 | if((d = readobject(dh)) == nil) 34 | sysfatal("bad hash %H", dh); 35 | assert(d->type == GTree); 36 | p = d->tree->ent; 37 | e = p + d->tree->nent; 38 | for(; p != e; p++){ 39 | if(p->ismod) 40 | continue; 41 | if(p->mode & DMDIR) 42 | showdir(p->h, p->name, m); 43 | else 44 | print("%c %P%s\n", m, p->name); 45 | } 46 | print("%c %P\n", m); 47 | unref(d); 48 | npath--; 49 | } 50 | 51 | void 52 | show(Dirent *e, char m) 53 | { 54 | if(e->mode & DMDIR) 55 | showdir(e->h, e->name, m); 56 | else 57 | print("%c %P%s\n", m, e->name); 58 | } 59 | 60 | void 61 | difftrees(Object *a, Object *b) 62 | { 63 | Dirent *ap, *bp, *ae, *be; 64 | int c; 65 | 66 | ap = ae = nil; 67 | bp = be = nil; 68 | if(a != nil){ 69 | if(a->type != GTree) 70 | return; 71 | ap = a->tree->ent; 72 | ae = ap + a->tree->nent; 73 | } 74 | if(b != nil){ 75 | if(b->type != GTree) 76 | return; 77 | bp = b->tree->ent; 78 | be = bp + b->tree->nent; 79 | } 80 | while(ap != ae && bp != be){ 81 | c = strcmp(ap->name, bp->name); 82 | if(c == 0){ 83 | if(ap->mode == bp->mode && hasheq(&ap->h, &bp->h)) 84 | goto next; 85 | if(ap->mode != bp->mode) 86 | print("! %P%s\n", ap->name); 87 | else if(!(ap->mode & DMDIR) || !(bp->mode & DMDIR)) 88 | print("@ %P%s\n", ap->name); 89 | if((ap->mode & DMDIR) && (bp->mode & DMDIR)){ 90 | if(npath >= nelem(path)) 91 | sysfatal("path too deep"); 92 | path[npath++] = ap->name; 93 | if((a = readobject(ap->h)) == nil) 94 | sysfatal("bad hash %H", ap->h); 95 | if((b = readobject(bp->h)) == nil) 96 | sysfatal("bad hash %H", bp->h); 97 | difftrees(a, b); 98 | unref(a); 99 | unref(b); 100 | npath--; 101 | } 102 | next: 103 | ap++; 104 | bp++; 105 | }else if(c < 0) { 106 | show(ap, '-'); 107 | ap++; 108 | }else if(c > 0){ 109 | show(bp, '+'); 110 | bp++; 111 | } 112 | } 113 | for(; ap != ae; ap++) 114 | show(ap, '-'); 115 | for(; bp != be; bp++) 116 | show(bp, '+'); 117 | } 118 | 119 | void 120 | diffcommits(Hash ah, Hash bh) 121 | { 122 | Object *a, *b, *at, *bt; 123 | 124 | at = nil; 125 | bt = nil; 126 | if(!hasheq(&ah, &Zhash) && (a = readobject(ah)) != nil){ 127 | if(a->type != GCommit) 128 | sysfatal("not commit: %H", ah); 129 | if((at = readobject(a->commit->tree)) == nil) 130 | sysfatal("bad hash %H", a->commit->tree); 131 | unref(a); 132 | } 133 | if(!hasheq(&bh, &Zhash) && (b = readobject(bh)) != nil){ 134 | if(b->type != GCommit) 135 | sysfatal("not commit: %H", ah); 136 | if((bt = readobject(b->commit->tree)) == nil) 137 | sysfatal("bad hash %H", b->commit->tree); 138 | unref(b); 139 | } 140 | difftrees(at, bt); 141 | unref(at); 142 | unref(bt); 143 | } 144 | 145 | void 146 | usage(void) 147 | { 148 | fprint(2, "usage: %s [-pcr] query\n", argv0); 149 | exits("usage"); 150 | } 151 | 152 | void 153 | main(int argc, char **argv) 154 | { 155 | char *query, repo[512]; 156 | char *p, *e, *objpfx; 157 | int i, j, n, nrel; 158 | Hash *h; 159 | 160 | ARGBEGIN{ 161 | case 'd': chattygit++; break; 162 | case 'p': fullpath++; break; 163 | case 'c': changes++; break; 164 | case 'r': reverse ^= 1; break; 165 | default: usage(); break; 166 | }ARGEND; 167 | 168 | gitinit(); 169 | fmtinstall('P', Pfmt); 170 | 171 | if(argc == 0) 172 | usage(); 173 | if(findrepo(repo, sizeof(repo), &nrel) == -1) 174 | sysfatal("find root: %r"); 175 | if(chdir(repo) == -1) 176 | sysfatal("chdir: %r"); 177 | if((objpfx = smprint("%s/.git/fs/object/", repo)) == nil) 178 | sysfatal("smprint: %r"); 179 | for(i = 0, n = 0; i < argc; i++) 180 | n += strlen(argv[i]) + 1; 181 | query = emalloc(n+1); 182 | p = query; 183 | e = query + n; 184 | for(i = 0; i < argc; i++) 185 | p = seprint(p, e, "%s ", argv[i]); 186 | n = resolverefs(&h, query); 187 | free(query); 188 | if(n == -1) 189 | sysfatal("resolve: %r"); 190 | if(changes){ 191 | for(i = 1; i < n; i++) 192 | diffcommits(h[0], h[i]); 193 | }else{ 194 | p = (fullpath ? objpfx : ""); 195 | for(j = 0; j < n; j++) 196 | print("%s%H\n", p, h[reverse ? n - 1 - j : j]); 197 | } 198 | exits(nil); 199 | } 200 | 201 | -------------------------------------------------------------------------------- /rebase: -------------------------------------------------------------------------------- 1 | #!/bin/rc 2 | 3 | . /sys/lib/git/common.rc 4 | gitup 5 | 6 | flagfmt='a:abort, r:resume, i:interactive'; args='onto' 7 | eval `''{aux/getflags $*} || exec aux/usage 8 | 9 | tmp=_rebase.working 10 | if(~ $#abort 1){ 11 | if(! test -f .git/rebase.todo) 12 | die no rebase to abort 13 | src=`{cat .git/rebase.src} 14 | rm -f .git/rebase.^(src todo) 15 | git/branch $src 16 | git/branch -d $tmp 17 | exit 18 | } 19 | if(test -f .git/rebase.todo){ 20 | if(~ $#resume 0) 21 | die rebase in progress 22 | if(! ~ $#* 0) 23 | exec aux/usage 24 | src=`{cat .git/rebase.src} 25 | } 26 | if not{ 27 | if(! ~ $#* 1) 28 | exec aux/usage 29 | src=`{git/branch} 30 | dst=`{git/query $1} 31 | echo $src > .git/rebase.src 32 | git/log -se $dst'..'$src | sed 's/^/pick /' >.git/rebase.todo 33 | if(! ~ $#interactive 0){ 34 | giteditor=`{git/conf core.editor} 35 | if(~ $#editor 0) 36 | editor=$giteditor 37 | if(~ $#editor 0) 38 | editor=hold 39 | $editor .git/rebase.todo 40 | } 41 | git/branch -nb $dst $tmp 42 | } 43 | todo=`$nl{cat .git/rebase.todo} 44 | 45 | fn sigexit { 46 | s=$status 47 | if(!) 48 | echo 'fix and git/rebase -r' 49 | >.git/rebase.todo for(i in $todo) 50 | echo $i 51 | status=$s 52 | } 53 | 54 | flag e + 55 | 56 | while(! ~ $#todo 0){ 57 | item=`{echo $todo(1)} 58 | todo=$todo(2-) 59 | echo $item 60 | c=$item(2) 61 | switch($item(1)){ 62 | case p pick 63 | git/export $c | git/import 64 | case r reword 65 | git/export $c | git/import 66 | git/commit -re 67 | case e edit 68 | git/export $c | git/import 69 | echo 'stopped for edit, resume with git/rebase -r' 70 | exit 71 | case s squash 72 | git/export $c | git/import -n 73 | msg=`''{cat $gitfs/HEAD/msg; echo; cat $gitfs/object/$c/msg} 74 | git/commit -rem $msg . 75 | case f fixup 76 | git/export $c | git/import -n 77 | git/commit -r . 78 | case b break 79 | echo 'stopped, resume with git/rebase -r' 80 | exit 81 | case '#'* '' 82 | case * 83 | die 'unknown command '''^$item(1)^'''' 84 | } 85 | } 86 | 87 | fn sigexit 88 | git/branch -nb $tmp $src 89 | git/branch -d $tmp 90 | rm .git/rebase.todo .git/rebase.src 91 | -------------------------------------------------------------------------------- /ref.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "git.h" 6 | 7 | typedef struct Eval Eval; 8 | 9 | enum { 10 | Blank, 11 | Keep, 12 | Drop, 13 | Skip, 14 | }; 15 | 16 | enum { 17 | Lca, 18 | Twixt, 19 | Range, 20 | }; 21 | 22 | struct Eval { 23 | char *str; 24 | char *p; 25 | Object **stk; 26 | int nstk; 27 | int stksz; 28 | }; 29 | 30 | static char *colors[] = { 31 | [Keep] "keep", 32 | [Drop] "drop", 33 | [Blank] "blank", 34 | [Skip] "skip", 35 | }; 36 | 37 | static Object zcommit = { 38 | .type=GCommit 39 | }; 40 | 41 | void 42 | eatspace(Eval *ev) 43 | { 44 | while(isspace(ev->p[0])) 45 | ev->p++; 46 | } 47 | 48 | void 49 | push(Eval *ev, Object *o) 50 | { 51 | if(ev->nstk == ev->stksz){ 52 | ev->stksz = 2*ev->stksz + 1; 53 | ev->stk = erealloc(ev->stk, ev->stksz*sizeof(Object*)); 54 | } 55 | ev->stk[ev->nstk++] = o; 56 | } 57 | 58 | Object* 59 | pop(Eval *ev) 60 | { 61 | if(ev->nstk == 0) 62 | sysfatal("stack underflow"); 63 | return ev->stk[--ev->nstk]; 64 | } 65 | 66 | Object* 67 | peek(Eval *ev) 68 | { 69 | if(ev->nstk == 0) 70 | sysfatal("stack underflow"); 71 | return ev->stk[ev->nstk - 1]; 72 | } 73 | 74 | int 75 | isword(char e) 76 | { 77 | return isalnum(e) || e == '/' || e == '-' || e == '_' || e == '.'; 78 | } 79 | 80 | int 81 | word(Eval *ev, char *b, int nb) 82 | { 83 | char *p, *e; 84 | int n; 85 | 86 | p = ev->p; 87 | for(e = p; isword(*e) && strncmp(e, "..", 2) != 0; e++) 88 | /* nothing */; 89 | /* 1 for nul terminator */ 90 | n = e - p + 1; 91 | if(n >= nb) 92 | n = nb; 93 | snprint(b, n, "%s", p); 94 | ev->p = e; 95 | return n > 0; 96 | } 97 | 98 | int 99 | take(Eval *ev, char *m) 100 | { 101 | int l; 102 | 103 | l = strlen(m); 104 | if(strncmp(ev->p, m, l) != 0) 105 | return 0; 106 | ev->p += l; 107 | return 1; 108 | } 109 | 110 | static int 111 | paint(Hash *head, int nhead, Hash *tail, int ntail, Object ***res, int *nres, int mode) 112 | { 113 | Qelt e; 114 | Objq objq; 115 | Objset keep, drop, skip; 116 | Object *o, *c, **range; 117 | int i, nskip, nrange; 118 | 119 | osinit(&keep); 120 | osinit(&drop); 121 | osinit(&skip); 122 | qinit(&objq); 123 | range = nil; 124 | nrange = 0; 125 | nskip = 0; 126 | 127 | for(i = 0; i < nhead; i++){ 128 | if(hasheq(&head[i], &Zhash)) 129 | continue; 130 | if((o = readobject(head[i])) == nil){ 131 | fprint(2, "warning: %H does not point at commit\n", head[i]); 132 | werrstr("read head %H: %r", head[i]); 133 | return -1; 134 | } 135 | if(o->type != GCommit){ 136 | fprint(2, "warning: %H does not point at commit\n", o->hash); 137 | unref(o); 138 | continue; 139 | } 140 | dprint(1, "init: keep %H\n", o->hash); 141 | qput(&objq, o, Keep); 142 | unref(o); 143 | } 144 | for(i = 0; i < ntail; i++){ 145 | if(hasheq(&tail[i], &Zhash)) 146 | continue; 147 | if((o = readobject(tail[i])) == nil){ 148 | werrstr("read tail %H: %r", tail[i]); 149 | return -1; 150 | } 151 | if(o->type != GCommit){ 152 | fprint(2, "warning: %H does not point at commit\n", o->hash); 153 | unref(o); 154 | continue; 155 | } 156 | dprint(1, "init: drop %H\n", o->hash); 157 | qput(&objq, o, Drop); 158 | unref(o); 159 | } 160 | 161 | dprint(1, "finding twixt commits\n"); 162 | while(nskip != objq.nheap && qpop(&objq, &e)){ 163 | if(e.color == Skip) 164 | nskip--; 165 | if(oshas(&skip, e.o->hash)) 166 | continue; 167 | switch(e.color){ 168 | case Keep: 169 | if(oshas(&keep, e.o->hash)) 170 | continue; 171 | if(oshas(&drop, e.o->hash)) 172 | e.color = Skip; 173 | else if(mode == Range){ 174 | range = earealloc(range, nrange+1, sizeof(Object*)); 175 | range[nrange++] = e.o; 176 | } 177 | osadd(&keep, e.o); 178 | break; 179 | case Drop: 180 | if(oshas(&drop, e.o->hash)) 181 | continue; 182 | if(oshas(&keep, e.o->hash)) 183 | e.color = Skip; 184 | osadd(&drop, e.o); 185 | break; 186 | case Skip: 187 | osadd(&skip, e.o); 188 | break; 189 | } 190 | o = readobject(e.o->hash); 191 | if(o->type != GCommit){ 192 | werrstr("not a commit: %H", o->hash); 193 | goto error; 194 | } 195 | for(i = 0; i < o->commit->nparent; i++){ 196 | if((c = readobject(e.o->commit->parent[i])) == nil) 197 | goto error; 198 | if(c->type != GCommit){ 199 | fprint(2, "warning: %H does not point at commit\n", c->hash); 200 | unref(c); 201 | continue; 202 | } 203 | dprint(2, "\tenqueue: %s %H\n", colors[e.color], c->hash); 204 | qput(&objq, c, e.color); 205 | unref(c); 206 | if(e.color == Skip) 207 | nskip++; 208 | } 209 | unref(o); 210 | } 211 | switch(mode){ 212 | case Lca: 213 | dprint(1, "found ancestor\n"); 214 | o = nil; 215 | for(i = 0; i < keep.sz; i++){ 216 | o = keep.obj[i]; 217 | if(o != nil && oshas(&drop, o->hash) && !oshas(&skip, o->hash)) 218 | break; 219 | } 220 | if(i == keep.sz){ 221 | *nres = 0; 222 | *res = nil; 223 | }else{ 224 | *nres = 1; 225 | *res = eamalloc(1, sizeof(Object*)); 226 | (*res)[0] = o; 227 | } 228 | break; 229 | case Twixt: 230 | dprint(1, "found twixt\n"); 231 | *res = eamalloc(keep.nobj, sizeof(Object*)); 232 | *nres = 0; 233 | for(i = 0; i < keep.sz; i++){ 234 | o = keep.obj[i]; 235 | if(o != nil && !oshas(&drop, o->hash) && !oshas(&skip, o->hash)){ 236 | (*res)[*nres] = o; 237 | (*nres)++; 238 | } 239 | } 240 | break; 241 | case Range: 242 | dprint(1, "found range\n"); 243 | *res = eamalloc(nrange, sizeof(Object*)); 244 | *nres = 0; 245 | for(i = nrange - 1; i >= 0; i--){ 246 | o = range[i]; 247 | if(!oshas(&drop, o->hash) && !oshas(&skip, o->hash)){ 248 | (*res)[*nres] = o; 249 | (*nres)++; 250 | } 251 | } 252 | free(range); 253 | break; 254 | } 255 | osclear(&keep); 256 | osclear(&drop); 257 | osclear(&skip); 258 | return 0; 259 | error: 260 | dprint(1, "paint error: %r\n"); 261 | free(objq.heap); 262 | free(range); 263 | return -1; 264 | } 265 | 266 | int 267 | findtwixt(Hash *head, int nhead, Hash *tail, int ntail, Object ***res, int *nres) 268 | { 269 | return paint(head, nhead, tail, ntail, res, nres, Twixt); 270 | } 271 | 272 | Object* 273 | ancestor(Object *a, Object *b) 274 | { 275 | Object **o, *r; 276 | int n; 277 | 278 | if(paint(&a->hash, 1, &b->hash, 1, &o, &n, Lca) == -1 || n == 0) 279 | return nil; 280 | r = ref(o[0]); 281 | free(o); 282 | return r; 283 | } 284 | 285 | int 286 | lca(Eval *ev) 287 | { 288 | Object *a, *b, **o; 289 | int n; 290 | 291 | if(ev->nstk < 2){ 292 | werrstr("ancestor needs 2 objects"); 293 | return -1; 294 | } 295 | n = 0; 296 | b = pop(ev); 297 | a = pop(ev); 298 | paint(&a->hash, 1, &b->hash, 1, &o, &n, Lca); 299 | if(n == 0) 300 | return -1; 301 | push(ev, *o); 302 | free(o); 303 | return 0; 304 | } 305 | 306 | static int 307 | parent(Eval *ev) 308 | { 309 | Object *o, *p; 310 | 311 | o = pop(ev); 312 | if(o->type != GCommit){ 313 | werrstr("not a commit: %H", o->hash); 314 | return -1; 315 | } 316 | /* Special case: first commit has no parent. */ 317 | if(o->commit->nparent == 0) 318 | p = emptydir(); 319 | else if ((p = readobject(o->commit->parent[0])) == nil){ 320 | werrstr("no parent for %H", o->hash); 321 | return -1; 322 | } 323 | 324 | push(ev, p); 325 | return 0; 326 | } 327 | 328 | static int 329 | range(Eval *ev) 330 | { 331 | Object *a, *b, **o; 332 | int i, n; 333 | 334 | b = pop(ev); 335 | a = pop(ev); 336 | if(hasheq(&b->hash, &Zhash)) 337 | b = &zcommit; 338 | if(hasheq(&a->hash, &Zhash)) 339 | a = &zcommit; 340 | if(a->type != GCommit || b->type != GCommit){ 341 | werrstr("non-commit object in range"); 342 | return -1; 343 | } 344 | 345 | if(paint(&b->hash, 1, &a->hash, 1, &o, &n, Range) == -1) 346 | return -1; 347 | for(i = 0; i < n; i++) 348 | push(ev, o[i]); 349 | free(o); 350 | return 0; 351 | } 352 | 353 | int 354 | readref(Hash *h, char *ref) 355 | { 356 | static char *try[] = {"", "refs/", "refs/heads/", "refs/remotes/", "refs/tags/", nil}; 357 | char buf[256], s[256], **pfx; 358 | int r, f, n; 359 | 360 | /* TODO: support hash prefixes */ 361 | if((r = hparse(h, ref)) != -1) 362 | return r; 363 | if(strcmp(ref, "HEAD") == 0){ 364 | snprint(buf, sizeof(buf), ".git/HEAD"); 365 | if((f = open(buf, OREAD)) == -1) 366 | return -1; 367 | if((n = readn(f, s, sizeof(s) - 1))== -1) 368 | return -1; 369 | s[n] = 0; 370 | strip(s); 371 | r = hparse(h, s); 372 | goto found; 373 | } 374 | for(pfx = try; *pfx; pfx++){ 375 | snprint(buf, sizeof(buf), ".git/%s%s", *pfx, ref); 376 | if((f = open(buf, OREAD)) == -1) 377 | continue; 378 | if((n = readn(f, s, sizeof(s) - 1)) == -1) 379 | continue; 380 | s[n] = 0; 381 | strip(s); 382 | r = hparse(h, s); 383 | close(f); 384 | goto found; 385 | } 386 | return -1; 387 | 388 | found: 389 | if(r == -1 && strstr(s, "ref: ") == s) 390 | r = readref(h, s + strlen("ref: ")); 391 | return r; 392 | } 393 | 394 | int 395 | evalpostfix(Eval *ev) 396 | { 397 | char name[256]; 398 | Object *o; 399 | Hash h; 400 | 401 | eatspace(ev); 402 | if(!word(ev, name, sizeof(name))){ 403 | werrstr("expected name in expression"); 404 | return -1; 405 | } 406 | if(readref(&h, name) == -1){ 407 | werrstr("invalid ref %s", name); 408 | return -1; 409 | } 410 | if(hasheq(&h, &Zhash)) 411 | o = &zcommit; 412 | else if((o = readobject(h)) == nil){ 413 | werrstr("invalid ref %s (hash %H)", name, h); 414 | return -1; 415 | } 416 | push(ev, o); 417 | 418 | while(1){ 419 | eatspace(ev); 420 | switch(ev->p[0]){ 421 | case '^': 422 | case '~': 423 | ev->p++; 424 | if(parent(ev) == -1) 425 | return -1; 426 | break; 427 | case '@': 428 | ev->p++; 429 | if(lca(ev) == -1) 430 | return -1; 431 | break; 432 | default: 433 | goto done; 434 | break; 435 | } 436 | } 437 | done: 438 | return 0; 439 | } 440 | 441 | int 442 | evalexpr(Eval *ev, char *ref) 443 | { 444 | memset(ev, 0, sizeof(*ev)); 445 | ev->str = ref; 446 | ev->p = ref; 447 | 448 | while(1){ 449 | if(evalpostfix(ev) == -1) 450 | return -1; 451 | if(ev->p[0] == '\0') 452 | return 0; 453 | else if(take(ev, ":") || take(ev, "..")){ 454 | if(evalpostfix(ev) == -1) 455 | return -1; 456 | if(ev->p[0] != '\0'){ 457 | werrstr("junk at end of expression"); 458 | return -1; 459 | } 460 | return range(ev); 461 | } 462 | } 463 | } 464 | 465 | int 466 | resolverefs(Hash **r, char *ref) 467 | { 468 | Eval ev; 469 | Hash *h; 470 | int i; 471 | 472 | if(evalexpr(&ev, ref) == -1){ 473 | free(ev.stk); 474 | return -1; 475 | } 476 | h = eamalloc(ev.nstk, sizeof(Hash)); 477 | for(i = 0; i < ev.nstk; i++) 478 | h[i] = ev.stk[i]->hash; 479 | *r = h; 480 | free(ev.stk); 481 | return ev.nstk; 482 | } 483 | 484 | int 485 | resolveref(Hash *r, char *ref) 486 | { 487 | Eval ev; 488 | 489 | if(evalexpr(&ev, ref) == -1){ 490 | free(ev.stk); 491 | return -1; 492 | } 493 | if(ev.nstk != 1){ 494 | werrstr("ambiguous ref expr"); 495 | free(ev.stk); 496 | return -1; 497 | } 498 | *r = ev.stk[0]->hash; 499 | free(ev.stk); 500 | return 0; 501 | } 502 | 503 | int 504 | readrefdir(Hash **refs, char ***names, int *nrefs, char *dpath, char *dname) 505 | { 506 | Dir *d, *e, *dir; 507 | char *path, *name, *sep; 508 | int ndir; 509 | 510 | if((ndir = slurpdir(dpath, &dir)) == -1) 511 | return -1; 512 | sep = (*dname == '\0') ? "" : "/"; 513 | e = dir + ndir; 514 | for(d = dir; d != e; d++){ 515 | path = smprint("%s/%s", dpath, d->name); 516 | name = smprint("%s%s%s", dname, sep, d->name); 517 | if(d->mode & DMDIR) { 518 | if(readrefdir(refs, names, nrefs, path, name) == -1) 519 | goto noref; 520 | }else{ 521 | *refs = erealloc(*refs, (*nrefs + 1)*sizeof(Hash)); 522 | *names = erealloc(*names, (*nrefs + 1)*sizeof(char*)); 523 | if(resolveref(&(*refs)[*nrefs], name) == -1) 524 | goto noref; 525 | (*names)[*nrefs] = name; 526 | *nrefs += 1; 527 | goto next; 528 | } 529 | noref: free(name); 530 | next: free(path); 531 | } 532 | free(dir); 533 | return 0; 534 | } 535 | 536 | int 537 | listrefs(Hash **refs, char ***names) 538 | { 539 | int nrefs; 540 | 541 | *refs = nil; 542 | *names = nil; 543 | nrefs = 0; 544 | if(readrefdir(refs, names, &nrefs, ".git/refs", "") == -1){ 545 | free(*refs); 546 | return -1; 547 | } 548 | return nrefs; 549 | } 550 | -------------------------------------------------------------------------------- /repack.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "git.h" 5 | 6 | #define TMPPATH(suff) (".git/objects/pack/repack."suff) 7 | 8 | int 9 | cleanup(Hash h) 10 | { 11 | char newpfx[42], dpath[256], fpath[256]; 12 | int i, j, nd; 13 | Dir *d; 14 | 15 | snprint(newpfx, sizeof(newpfx), "%H.", h); 16 | for(i = 0; i < 256; i++){ 17 | snprint(dpath, sizeof(dpath), ".git/objects/%02x", i); 18 | if((nd = slurpdir(dpath, &d)) == -1) 19 | continue; 20 | for(j = 0; j < nd; j++){ 21 | snprint(fpath, sizeof(fpath), ".git/objects/%02x/%s", i, d[j].name); 22 | remove(fpath); 23 | } 24 | remove(dpath); 25 | free(d); 26 | } 27 | snprint(dpath, sizeof(dpath), ".git/objects/pack"); 28 | if((nd = slurpdir(dpath, &d)) == -1) 29 | return -1; 30 | for(i = 0; i < nd; i++){ 31 | if(strncmp(d[i].name, newpfx, strlen(newpfx)) == 0) 32 | continue; 33 | snprint(fpath, sizeof(fpath), ".git/objects/pack/%s", d[i].name); 34 | remove(fpath); 35 | } 36 | return 0; 37 | } 38 | 39 | void 40 | usage(void) 41 | { 42 | fprint(2, "usage: %s [-d]\n", argv0); 43 | exits("usage"); 44 | } 45 | 46 | void 47 | main(int argc, char **argv) 48 | { 49 | char path[128], **names; 50 | int fd, nrefs; 51 | Hash *refs, h; 52 | Dir rn; 53 | 54 | ARGBEGIN{ 55 | case 'd': 56 | chattygit++; 57 | break; 58 | default: 59 | usage(); 60 | }ARGEND; 61 | 62 | gitinit(); 63 | refs = nil; 64 | if((nrefs = listrefs(&refs, &names)) == -1) 65 | sysfatal("load refs: %r"); 66 | if((fd = create(TMPPATH("pack.tmp"), OWRITE, 0644)) == -1) 67 | sysfatal("open %s: %r", TMPPATH("pack.tmp")); 68 | if(writepack(fd, refs, nrefs, nil, 0, &h) == -1) 69 | sysfatal("writepack: %r"); 70 | if(indexpack(TMPPATH("pack.tmp"), TMPPATH("idx.tmp"), h) == -1) 71 | sysfatal("indexpack: %r"); 72 | close(fd); 73 | 74 | nulldir(&rn); 75 | rn.name = path; 76 | snprint(path, sizeof(path), "%H.pack", h); 77 | if(dirwstat(TMPPATH("pack.tmp"), &rn) == -1) 78 | sysfatal("rename pack: %r"); 79 | snprint(path, sizeof(path), "%H.idx", h); 80 | if(dirwstat(TMPPATH("idx.tmp"), &rn) == -1) 81 | sysfatal("rename pack: %r"); 82 | if(cleanup(h) == -1) 83 | sysfatal("cleanup: %r"); 84 | exits(nil); 85 | } 86 | -------------------------------------------------------------------------------- /revert: -------------------------------------------------------------------------------- 1 | #!/bin/rc 2 | rfork en 3 | . /sys/lib/git/common.rc 4 | 5 | gitup 6 | 7 | flagfmt='c:query query' args='file ...' 8 | if (! eval `''{aux/getflags $*} || ~ $#* 0) 9 | exec aux/usage 10 | 11 | if(~ $#query 0) 12 | query=HEAD 13 | commit=`{git/query -p $query} 14 | 15 | files=`$nl{cleanname -d $gitrel $* | drop $gitroot} 16 | for(f in `$nl{git/walk -c -fRM -b $query $files}){ 17 | mkdir -p `{basename -d $f} 18 | cp -x -- $commit/tree/$f $f 19 | touch $f 20 | git/add $f 21 | } 22 | exit '' 23 | -------------------------------------------------------------------------------- /rm: -------------------------------------------------------------------------------- 1 | #!/bin/rc -e 2 | 3 | exec git/add -r $* 4 | -------------------------------------------------------------------------------- /save.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "git.h" 4 | 5 | typedef struct Objbuf Objbuf; 6 | struct Objbuf { 7 | int off; 8 | char *hdr; 9 | int nhdr; 10 | char *dat; 11 | int ndat; 12 | }; 13 | 14 | enum { 15 | Maxparents = 16, 16 | }; 17 | 18 | char *authorname; 19 | char *authoremail; 20 | char *committername; 21 | char *committeremail; 22 | char *commitmsg; 23 | Hash parents[Maxparents]; 24 | int nparents; 25 | Idxent *idx; 26 | int idxsz; 27 | int nidx; 28 | int 29 | gitmode(Dirent *e) 30 | { 31 | if(e->islink) 32 | return 0120000; 33 | else if(e->ismod) 34 | return 0160000; 35 | else if(e->mode & DMDIR) 36 | return 0040000; 37 | else if(e->mode & 0100) 38 | return 0100755; 39 | else 40 | return 0100644; 41 | } 42 | 43 | int 44 | idxcmp(void *pa, void *pb) 45 | { 46 | Idxent *a, *b; 47 | int c; 48 | 49 | a = (Idxent*)pa; 50 | b = (Idxent*)pb; 51 | if((c = strcmp(a->path, b->path)) != 0) 52 | return c; 53 | assert(a->order != b->order); 54 | return a-> order < b->order ? -1 : 1; 55 | } 56 | 57 | int 58 | entcmp(void *pa, void *pb) 59 | { 60 | char abuf[256], bbuf[256], *ae, *be; 61 | Dirent *a, *b; 62 | 63 | a = pa; 64 | b = pb; 65 | /* 66 | * If the files have the same name, they're equal. 67 | * Otherwise, If they're trees, they sort as thoug 68 | * there was a trailing slash. 69 | * 70 | * Wat. 71 | */ 72 | if(strcmp(a->name, b->name) == 0) 73 | return 0; 74 | 75 | ae = seprint(abuf, abuf + sizeof(abuf) - 1, a->name); 76 | be = seprint(bbuf, bbuf + sizeof(bbuf) - 1, b->name); 77 | if(a->mode & DMDIR) 78 | *ae = '/'; 79 | if(b->mode & DMDIR) 80 | *be = '/'; 81 | return strcmp(abuf, bbuf); 82 | } 83 | 84 | static int 85 | bwrite(void *p, void *buf, int nbuf) 86 | { 87 | return Bwrite(p, buf, nbuf); 88 | } 89 | 90 | static int 91 | objbytes(void *p, void *buf, int nbuf) 92 | { 93 | Objbuf *b; 94 | int r, n, o; 95 | char *s; 96 | 97 | b = p; 98 | n = 0; 99 | if(b->off < b->nhdr){ 100 | r = b->nhdr - b->off; 101 | r = (nbuf < r) ? nbuf : r; 102 | memcpy(buf, b->hdr, r); 103 | b->off += r; 104 | nbuf -= r; 105 | n += r; 106 | } 107 | if(b->off < b->ndat + b->nhdr){ 108 | s = buf; 109 | o = b->off - b->nhdr; 110 | r = b->ndat - o; 111 | r = (nbuf < r) ? nbuf : r; 112 | memcpy(s + n, b->dat + o, r); 113 | b->off += r; 114 | n += r; 115 | } 116 | return n; 117 | } 118 | 119 | void 120 | writeobj(Hash *h, char *hdr, int nhdr, char *dat, int ndat) 121 | { 122 | Objbuf b = {.off=0, .hdr=hdr, .nhdr=nhdr, .dat=dat, .ndat=ndat}; 123 | char s[64], o[256]; 124 | SHA1state *st; 125 | Biobuf *f; 126 | int fd; 127 | 128 | st = sha1((uchar*)hdr, nhdr, nil, nil); 129 | st = sha1((uchar*)dat, ndat, nil, st); 130 | sha1(nil, 0, h->h, st); 131 | 132 | snprint(s, sizeof(s), "%H", *h); 133 | fd = create(".git/objects", OREAD, DMDIR|0755); 134 | close(fd); 135 | snprint(o, sizeof(o), ".git/objects/%c%c", s[0], s[1]); 136 | fd = create(o, OREAD, DMDIR | 0755); 137 | close(fd); 138 | snprint(o, sizeof(o), ".git/objects/%c%c/%s", s[0], s[1], s + 2); 139 | if(readobject(*h) == nil){ 140 | if((f = Bopen(o, OWRITE)) == nil) 141 | sysfatal("could not open %s: %r", o); 142 | if(deflatezlib(f, bwrite, &b, objbytes, 9, 0) == -1) 143 | sysfatal("could not write %s: %r", o); 144 | Bterm(f); 145 | } 146 | } 147 | 148 | int 149 | writetree(Dirent *ent, int nent, Hash *h) 150 | { 151 | char *t, *txt, *etxt, hdr[128]; 152 | int nhdr, n; 153 | Dirent *d, *p; 154 | 155 | t = emalloc((16+256+20) * nent); 156 | txt = t; 157 | etxt = t + (16+256+20) * nent; 158 | 159 | /* sqeeze out deleted entries */ 160 | n = 0; 161 | p = ent; 162 | for(d = ent; d != ent + nent; d++) 163 | if(d->name) 164 | p[n++] = *d; 165 | nent = n; 166 | 167 | qsort(ent, nent, sizeof(Dirent), entcmp); 168 | for(d = ent; d != ent + nent; d++){ 169 | if(strlen(d->name) >= 255) 170 | sysfatal("overly long filename: %s", d->name); 171 | t = seprint(t, etxt, "%o %s", gitmode(d), d->name) + 1; 172 | memcpy(t, d->h.h, sizeof(d->h.h)); 173 | t += sizeof(d->h.h); 174 | } 175 | nhdr = snprint(hdr, sizeof(hdr), "%T %lld", GTree, (vlong)(t - txt)) + 1; 176 | writeobj(h, hdr, nhdr, txt, t - txt); 177 | free(txt); 178 | return nent; 179 | } 180 | 181 | void 182 | blobify(Dir *d, char *path, int *mode, Hash *bh) 183 | { 184 | char h[64], *buf; 185 | int f, nh; 186 | 187 | if((d->mode & DMDIR) != 0) 188 | sysfatal("not file: %s", path); 189 | *mode = d->mode; 190 | nh = snprint(h, sizeof(h), "%T %lld", GBlob, d->length) + 1; 191 | if((f = open(path, OREAD)) == -1) 192 | sysfatal("could not open %s: %r", path); 193 | buf = emalloc(d->length); 194 | if(readn(f, buf, d->length) != d->length) 195 | sysfatal("could not read blob %s: %r", path); 196 | writeobj(bh, h, nh, buf, d->length); 197 | free(buf); 198 | close(f); 199 | } 200 | 201 | int 202 | tracked(char *path) 203 | { 204 | int r, lo, hi, mid; 205 | 206 | lo = 0; 207 | hi = nidx-1; 208 | while(lo <= hi){ 209 | mid = (hi + lo) / 2; 210 | r = strcmp(path, idx[mid].path); 211 | if(r < 0) 212 | hi = mid-1; 213 | else if(r > 0) 214 | lo = mid+1; 215 | else 216 | return idx[mid].state != 'R'; 217 | } 218 | return 0; 219 | } 220 | 221 | int 222 | pathelt(char *buf, int nbuf, char *p, int *isdir) 223 | { 224 | char *b; 225 | 226 | b = buf; 227 | if(*p == '/') 228 | p++; 229 | while(*p && *p != '/' && b != buf + nbuf) 230 | *b++ = *p++; 231 | *b = '\0'; 232 | *isdir = (*p == '/'); 233 | return b - buf; 234 | } 235 | 236 | Dirent* 237 | dirent(Dirent **ent, int *nent, char *name) 238 | { 239 | Dirent *d; 240 | 241 | for(d = *ent; d != *ent + *nent; d++) 242 | if(d->name && strcmp(d->name, name) == 0) 243 | return d; 244 | *nent += 1; 245 | *ent = erealloc(*ent, *nent * sizeof(Dirent)); 246 | d = *ent + (*nent - 1); 247 | memset(d, 0, sizeof(*d)); 248 | d->name = estrdup(name); 249 | return d; 250 | } 251 | 252 | int 253 | treeify(Object *t, char **path, char **epath, int off, Hash *h) 254 | { 255 | int r, n, ne, nsub, nent, isdir; 256 | char **p, **ep; 257 | char elt[256]; 258 | Object **sub; 259 | Dirent *e, *ent; 260 | Dir *d; 261 | 262 | r = -1; 263 | nsub = 0; 264 | nent = t->tree->nent; 265 | ent = eamalloc(nent, sizeof(*ent)); 266 | sub = eamalloc((epath - path), sizeof(Object*)); 267 | memcpy(ent, t->tree->ent, nent*sizeof(*ent)); 268 | for(p = path; p != epath; p = ep){ 269 | ne = pathelt(elt, sizeof(elt), *p + off, &isdir); 270 | for(ep = p; ep != epath; ep++){ 271 | if(strncmp(elt, *ep + off, ne) != 0) 272 | break; 273 | if((*ep)[off+ne] != '\0' && (*ep)[off+ne] != '/') 274 | break; 275 | } 276 | e = dirent(&ent, &nent, elt); 277 | if(e->islink) 278 | sysfatal("symlinks may not be modified: %s", *path); 279 | if(e->ismod) 280 | sysfatal("submodules may not be modified: %s", *path); 281 | if(isdir){ 282 | e->mode = DMDIR | 0755; 283 | sub[nsub] = readobject(e->h); 284 | if(sub[nsub] == nil || sub[nsub]->type != GTree) 285 | sub[nsub] = emptydir(); 286 | /* 287 | * if after processing deletions, a tree is empty, 288 | * mark it for removal from the parent. 289 | * 290 | * Note, it is still written to the object store, 291 | * but this is fine -- and ensures that an empty 292 | * repository will continue to work. 293 | */ 294 | n = treeify(sub[nsub], p, ep, off + ne + 1, &e->h); 295 | if(n == 0) 296 | e->name = nil; 297 | else if(n == -1) 298 | goto err; 299 | }else{ 300 | d = dirstat(*p); 301 | if(d != nil && tracked(*p)) 302 | blobify(d, *p, &e->mode, &e->h); 303 | else 304 | e->name = nil; 305 | free(d); 306 | } 307 | } 308 | if(nent == 0){ 309 | werrstr("%.*s: empty directory", off, *path); 310 | goto err; 311 | } 312 | 313 | r = writetree(ent, nent, h); 314 | err: 315 | free(sub); 316 | return r; 317 | } 318 | 319 | 320 | void 321 | mkcommit(Hash *c, vlong date, Hash tree) 322 | { 323 | char *s, h[64]; 324 | int ns, nh, i; 325 | Fmt f; 326 | 327 | fmtstrinit(&f); 328 | fmtprint(&f, "tree %H\n", tree); 329 | for(i = 0; i < nparents; i++) 330 | fmtprint(&f, "parent %H\n", parents[i]); 331 | fmtprint(&f, "author %s <%s> %lld +0000\n", authorname, authoremail, date); 332 | fmtprint(&f, "committer %s <%s> %lld +0000\n", committername, committeremail, date); 333 | fmtprint(&f, "\n"); 334 | fmtprint(&f, "%s", commitmsg); 335 | s = fmtstrflush(&f); 336 | 337 | ns = strlen(s); 338 | nh = snprint(h, sizeof(h), "%T %d", GCommit, ns) + 1; 339 | writeobj(c, h, nh, s, ns); 340 | free(s); 341 | } 342 | 343 | Object* 344 | findroot(void) 345 | { 346 | Object *t, *c; 347 | Hash h; 348 | 349 | if(resolveref(&h, "HEAD") == -1) 350 | return emptydir(); 351 | if((c = readobject(h)) == nil || c->type != GCommit) 352 | sysfatal("could not read HEAD %H", h); 353 | if((t = readobject(c->commit->tree)) == nil) 354 | sysfatal("could not read tree for commit %H", h); 355 | return t; 356 | } 357 | 358 | void 359 | usage(void) 360 | { 361 | fprint(2, "usage: %s -n name -e email -m message -d date [files...]\n", argv0); 362 | exits("usage"); 363 | } 364 | 365 | void 366 | main(int argc, char **argv) 367 | { 368 | char *ln, *dstr, *parts[4], cwd[1024]; 369 | int i, r, line, ncwd; 370 | Hash th, ch; 371 | vlong date; 372 | Biobuf *f; 373 | Object *t; 374 | 375 | gitinit(); 376 | if(access(".git", AEXIST) != 0) 377 | sysfatal("could not find git repo: %r"); 378 | if(getwd(cwd, sizeof(cwd)) == nil) 379 | sysfatal("getcwd: %r"); 380 | dstr = nil; 381 | date = time(nil); 382 | ncwd = strlen(cwd); 383 | 384 | ARGBEGIN{ 385 | case 'm': 386 | commitmsg = EARGF(usage()); 387 | break; 388 | case 'n': 389 | authorname = EARGF(usage()); 390 | break; 391 | case 'e': 392 | authoremail = EARGF(usage()); 393 | break; 394 | case 'N': 395 | committername = EARGF(usage()); 396 | break; 397 | case 'E': 398 | committeremail = EARGF(usage()); 399 | break; 400 | case 'd': 401 | dstr = EARGF(usage()); 402 | break; 403 | case 'p': 404 | if(nparents >= Maxparents) 405 | sysfatal("too many parents"); 406 | if(resolveref(&parents[nparents++], EARGF(usage())) == -1) 407 | sysfatal("invalid parent: %r"); 408 | break; 409 | default: 410 | usage(); 411 | break; 412 | }ARGEND; 413 | 414 | if(commitmsg == nil) 415 | sysfatal("missing message"); 416 | if(authorname == nil) 417 | sysfatal("missing name"); 418 | if(authoremail == nil) 419 | sysfatal("missing email"); 420 | if((committername == nil) != (committeremail == nil)) 421 | sysfatal("partially specified committer"); 422 | if(committername == nil && committeremail == nil){ 423 | committername = authorname; 424 | committeremail = authoremail; 425 | } 426 | if(dstr){ 427 | date=strtoll(dstr, &dstr, 10); 428 | if(strlen(dstr) != 0) 429 | sysfatal("could not parse date %s", dstr); 430 | } 431 | for(i = 0; i < argc; i++){ 432 | cleanname(argv[i]); 433 | if(*argv[i] == '/' && strncmp(argv[i], cwd, ncwd) == 0) 434 | argv[i] += ncwd; 435 | while(*argv[i] == '/') 436 | argv[i]++; 437 | } 438 | 439 | t = findroot(); 440 | nidx = 0; 441 | idxsz = 32; 442 | idx = emalloc(idxsz*sizeof(Idxent)); 443 | if((f = Bopen(".git/INDEX9", OREAD)) == nil) 444 | sysfatal("open index: %r"); 445 | line = 0; 446 | while((ln = Brdstr(f, '\n', 1)) != nil){ 447 | line++; 448 | if(ln[0] == 0 || ln[0] == '\n') 449 | continue; 450 | if(getfields(ln, parts, nelem(parts), 0, " \t") != nelem(parts)) 451 | sysfatal(".git/INDEX9:%d: corrupt index", line); 452 | if(nidx == idxsz){ 453 | idxsz += idxsz/2; 454 | idx = realloc(idx, idxsz*sizeof(Idxent)); 455 | } 456 | cleanname(parts[3]); 457 | idx[nidx].state = *parts[0]; 458 | idx[nidx].qid = parseqid(parts[1]); 459 | idx[nidx].mode = strtol(parts[2], nil, 8); 460 | idx[nidx].path = strdup(parts[3]); 461 | idx[nidx].order = nidx; 462 | nidx++; 463 | free(ln); 464 | } 465 | Bterm(f); 466 | qsort(idx, nidx, sizeof(Idxent), idxcmp); 467 | r = treeify(t, argv, argv + argc, 0, &th); 468 | if(r == -1) 469 | sysfatal("could not commit: %r\n"); 470 | mkcommit(&ch, date, th); 471 | print("%H\n", ch); 472 | exits(nil); 473 | } 474 | -------------------------------------------------------------------------------- /send.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "git.h" 5 | 6 | typedef struct Capset Capset; 7 | typedef struct Map Map; 8 | 9 | struct Capset { 10 | int sideband; 11 | int sideband64k; 12 | int report; 13 | }; 14 | 15 | struct Map { 16 | char *ref; 17 | Hash ours; 18 | Hash theirs; 19 | }; 20 | 21 | int sendall; 22 | int force; 23 | int nbranch; 24 | char **branch; 25 | char *removed[128]; 26 | int nremoved; 27 | int npacked; 28 | int nsent; 29 | 30 | int 31 | findref(char **r, int nr, char *ref) 32 | { 33 | int i; 34 | for(i = 0; i < nr; i++) 35 | if(strcmp(r[i], ref) == 0) 36 | return i; 37 | return -1; 38 | } 39 | 40 | int 41 | findkey(Map *m, int nm, char *ref) 42 | { 43 | int i; 44 | for(i = 0; i < nm; i++) 45 | if(strcmp(m[i].ref, ref) == 0) 46 | return i; 47 | return -1; 48 | } 49 | 50 | int 51 | readours(Hash **tailp, char ***refp) 52 | { 53 | int nu, i, idx; 54 | char *r, *pfx, **ref; 55 | Hash *tail; 56 | 57 | if(sendall) 58 | return listrefs(tailp, refp); 59 | nu = 0; 60 | tail = eamalloc((nremoved + nbranch), sizeof(Hash)); 61 | ref = eamalloc((nremoved + nbranch), sizeof(char*)); 62 | for(i = 0; i < nbranch; i++){ 63 | ref[nu] = estrdup(branch[i]); 64 | if(resolveref(&tail[nu], branch[i]) == -1) 65 | sysfatal("broken branch %s", branch[i]); 66 | nu++; 67 | } 68 | for(i = 0; i < nremoved; i++){ 69 | pfx = "refs/heads/"; 70 | if(strstr(removed[i], "heads/") == removed[i]) 71 | pfx = "refs/"; 72 | if(strstr(removed[i], "refs/heads/") == removed[i]) 73 | pfx = ""; 74 | if((r = smprint("%s%s", pfx, removed[i])) == nil) 75 | sysfatal("smprint: %r"); 76 | if((idx = findref(ref, nu, r)) == -1) 77 | idx = nu++; 78 | else 79 | free(ref[idx]); 80 | assert(idx < nremoved + nbranch); 81 | memcpy(&tail[idx], &Zhash, sizeof(Hash)); 82 | ref[idx] = r; 83 | } 84 | dprint(1, "nu: %d\n", nu); 85 | for(i = 0; i < nu; i++) 86 | dprint(1, "update: %H %s\n", tail[i], ref[i]); 87 | *tailp = tail; 88 | *refp = ref; 89 | return nu; 90 | } 91 | 92 | char * 93 | matchcap(char *s, char *cap, int full) 94 | { 95 | if(strncmp(s, cap, strlen(cap)) == 0) 96 | if(!full || strlen(s) == strlen(cap)) 97 | return s + strlen(cap); 98 | return nil; 99 | } 100 | 101 | void 102 | parsecaps(char *caps, Capset *cs) 103 | { 104 | char *p, *n; 105 | 106 | for(p = caps; p != nil; p = n){ 107 | n = strchr(p, ' '); 108 | if(n != nil) 109 | *n++ = 0; 110 | if(matchcap(p, "report-status", 1) != nil) 111 | cs->report = 1; 112 | if(matchcap(p, "side-band", 1) != nil) 113 | cs->sideband = 1; 114 | if(matchcap(p, "side-band-64k", 1) != nil) 115 | cs->sideband64k = 1; 116 | } 117 | } 118 | 119 | int 120 | sendpack(Conn *c) 121 | { 122 | int i, n, idx, nsp, send, first; 123 | int nours, ntheirs, nmap; 124 | char buf[Pktmax], *sp[3]; 125 | Hash h, *theirs, *ours; 126 | Object *a, *b, *p, *o; 127 | char **refs; 128 | Capset cs; 129 | Map *map, *m; 130 | 131 | first = 1; 132 | memset(&cs, 0, sizeof(Capset)); 133 | nours = readours(&ours, &refs); 134 | theirs = nil; 135 | ntheirs = 0; 136 | nmap = nours; 137 | map = eamalloc(nmap, sizeof(Map)); 138 | for(i = 0; i < nmap; i++){ 139 | map[i].theirs = Zhash; 140 | map[i].ours = ours[i]; 141 | map[i].ref = refs[i]; 142 | } 143 | while(1){ 144 | n = readpkt(c, buf, sizeof(buf)); 145 | if(n == -1) 146 | return -1; 147 | if(n == 0) 148 | break; 149 | if(first && n > strlen(buf)) 150 | parsecaps(buf + strlen(buf) + 1, &cs); 151 | first = 0; 152 | 153 | if(getfields(buf, sp, nelem(sp), 1, " \t\r\n") != 2) 154 | sysfatal("invalid ref line %.*s", utfnlen(buf, n), buf); 155 | theirs = earealloc(theirs, ntheirs+1, sizeof(Hash)); 156 | if(hparse(&theirs[ntheirs], sp[0]) == -1) 157 | sysfatal("invalid hash %s", sp[0]); 158 | if((idx = findkey(map, nmap, sp[1])) != -1) 159 | map[idx].theirs = theirs[ntheirs]; 160 | /* 161 | * we only keep their ref if we can read the object to add it 162 | * to our reachability; otherwise, discard it; we only care 163 | * that we don't have it, so we can tell whether we need to 164 | * bail out of pushing. 165 | */ 166 | if((o = readobject(theirs[ntheirs])) != nil){ 167 | ntheirs++; 168 | unref(o); 169 | } 170 | } 171 | 172 | if(writephase(c) == -1) 173 | return -1; 174 | send = 0; 175 | if(force) 176 | send=1; 177 | for(i = 0; i < nmap; i++){ 178 | m = &map[i]; 179 | a = readobject(m->theirs); 180 | if(hasheq(&m->ours, &Zhash)) 181 | b = nil; 182 | else 183 | b = readobject(m->ours); 184 | p = nil; 185 | if(a != nil && b != nil) 186 | p = ancestor(a, b); 187 | if(!force 188 | && !hasheq(&m->theirs, &Zhash) 189 | && !hasheq(&m->ours, &Zhash) 190 | && (a == nil || p != a)){ 191 | fprint(2, "remote has diverged\n"); 192 | werrstr("remote diverged"); 193 | flushpkt(c); 194 | return -1; 195 | } 196 | unref(a); 197 | unref(b); 198 | unref(p); 199 | if(hasheq(&m->theirs, &m->ours)){ 200 | print("uptodate %s\n", m->ref); 201 | continue; 202 | } 203 | print("update %s %H %H\n", m->ref, m->theirs, m->ours); 204 | n = snprint(buf, sizeof(buf), "%H %H %s", m->theirs, m->ours, m->ref); 205 | 206 | /* 207 | * Workaround for github. 208 | * 209 | * Github will accept the pack but fail to update the references 210 | * if we don't have capabilities advertised. Report-status seems 211 | * harmless to add, so we add it. 212 | * 213 | * Github doesn't advertise any capabilities, so we can't check 214 | * for compatibility. We just need to add it blindly. 215 | */ 216 | if(i == 0 && cs.report){ 217 | buf[n++] = '\0'; 218 | n += snprint(buf + n, sizeof(buf) - n, " report-status"); 219 | } 220 | if(writepkt(c, buf, n) == -1) 221 | sysfatal("unable to send update pkt"); 222 | send = 1; 223 | } 224 | flushpkt(c); 225 | if(!send){ 226 | fprint(2, "nothing to send\n"); 227 | return 0; 228 | } 229 | 230 | if(writepack(c->wfd, ours, nours, theirs, ntheirs, &h) == -1) 231 | return -1; 232 | if(!cs.report) 233 | return 0; 234 | 235 | if(readphase(c) == -1) 236 | return -1; 237 | /* We asked for a status report, may as well use it. */ 238 | while((n = readpkt(c, buf, sizeof(buf))) > 0){ 239 | buf[n] = 0; 240 | if(chattygit) 241 | fprint(2, "done sending pack, status %s\n", buf); 242 | nsp = getfields(buf, sp, nelem(sp), 1, " \t\n\r"); 243 | if(nsp < 2) 244 | continue; 245 | if(nsp < 3) 246 | sp[2] = ""; 247 | /* 248 | * Only report errors; successes will be reported by 249 | * surrounding scripts. 250 | */ 251 | if(strcmp(sp[0], "unpack") == 0 && strcmp(sp[1], "ok") != 0) 252 | fprint(2, "unpack %s\n", sp[1]); 253 | else if(strcmp(sp[0], "ng") == 0) 254 | fprint(2, "failed update: %s\n", sp[1]); 255 | else 256 | continue; 257 | return -1; 258 | } 259 | return 0; 260 | } 261 | 262 | void 263 | usage(void) 264 | { 265 | fprint(2, "usage: %s remote [reponame]\n", argv0); 266 | exits("usage"); 267 | } 268 | 269 | void 270 | main(int argc, char **argv) 271 | { 272 | char *br; 273 | Conn c; 274 | 275 | ARGBEGIN{ 276 | default: 277 | usage(); 278 | break; 279 | case 'd': 280 | chattygit++; 281 | break; 282 | case 'f': 283 | force++; 284 | break; 285 | case 'r': 286 | if(nremoved == nelem(removed)) 287 | sysfatal("too many deleted branches"); 288 | removed[nremoved++] = EARGF(usage()); 289 | break; 290 | case 'a': 291 | sendall++; 292 | break; 293 | case 'b': 294 | br = EARGF(usage()); 295 | if(strncmp(br, "refs/heads/", strlen("refs/heads/")) == 0) 296 | br = smprint("%s", br); 297 | else if(strncmp(br, "heads/", strlen("heads/")) == 0) 298 | br = smprint("refs/%s", br); 299 | else 300 | br = smprint("refs/heads/%s", br); 301 | branch = erealloc(branch, (nbranch + 1)*sizeof(char*)); 302 | branch[nbranch] = br; 303 | nbranch++; 304 | break; 305 | }ARGEND; 306 | 307 | gitinit(); 308 | if(argc != 1) 309 | usage(); 310 | if(gitconnect(&c, argv[0], "receive") == -1) 311 | sysfatal("git connect: %s: %r", argv[0]); 312 | if(sendpack(&c) == -1) 313 | sysfatal("send failed: %r"); 314 | closeconn(&c); 315 | exits(nil); 316 | } 317 | -------------------------------------------------------------------------------- /serve.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "git.h" 7 | 8 | char *pathpfx = nil; 9 | int allowwrite; 10 | 11 | _Noreturn static void 12 | fail(Conn *c, char *fmt, ...) 13 | { 14 | char msg[ERRMAX]; 15 | va_list ap; 16 | 17 | va_start(ap, fmt); 18 | vsnprint(msg, sizeof(msg), fmt, ap); 19 | va_end(ap); 20 | fmtpkt(c, "ERR %s\n", msg); 21 | sysfatal("%s", msg); 22 | } 23 | 24 | int 25 | showrefs(Conn *c) 26 | { 27 | int i, ret, nrefs; 28 | Hash head, *refs; 29 | char **names; 30 | 31 | ret = -1; 32 | nrefs = 0; 33 | refs = nil; 34 | names = nil; 35 | if(resolveref(&head, "HEAD") != -1) 36 | if(fmtpkt(c, "%H HEAD\n", head) == -1) 37 | goto error; 38 | 39 | if((nrefs = listrefs(&refs, &names)) == -1) 40 | fail(c, "listrefs: %r"); 41 | for(i = 0; i < nrefs; i++){ 42 | if(strncmp(names[i], "heads/", strlen("heads/")) != 0) 43 | continue; 44 | if(fmtpkt(c, "%H refs/%s\n", refs[i], names[i]) == -1) 45 | goto error; 46 | } 47 | if(flushpkt(c) == -1) 48 | goto error; 49 | ret = 0; 50 | error: 51 | for(i = 0; i < nrefs; i++) 52 | free(names[i]); 53 | free(names); 54 | free(refs); 55 | return ret; 56 | } 57 | 58 | int 59 | servnegotiate(Conn *c, Hash **head, int *nhead, Hash **tail, int *ntail) 60 | { 61 | char pkt[Pktmax]; 62 | int n, acked; 63 | Object *o; 64 | Hash h; 65 | 66 | if(showrefs(c) == -1) 67 | return -1; 68 | 69 | *head = nil; 70 | *tail = nil; 71 | *nhead = 0; 72 | *ntail = 0; 73 | while(1){ 74 | if((n = readpkt(c, pkt, sizeof(pkt))) == -1) 75 | goto error; 76 | if(n == 0) 77 | break; 78 | if(strncmp(pkt, "want ", 5) != 0){ 79 | werrstr(" protocol garble %s", pkt); 80 | goto error; 81 | } 82 | if(hparse(&h, &pkt[5]) == -1){ 83 | werrstr(" garbled want"); 84 | goto error; 85 | } 86 | if((o = readobject(h)) == nil){ 87 | werrstr("requested nonexistent object"); 88 | goto error; 89 | } 90 | unref(o); 91 | *head = erealloc(*head, (*nhead + 1)*sizeof(Hash)); 92 | (*head)[*nhead] = h; 93 | *nhead += 1; 94 | } 95 | 96 | acked = 0; 97 | while(1){ 98 | if((n = readpkt(c, pkt, sizeof(pkt))) == -1) 99 | goto error; 100 | if(strncmp(pkt, "done", 4) == 0) 101 | break; 102 | if(n == 0){ 103 | if(!acked && fmtpkt(c, "NAK") == -1) 104 | goto error; 105 | } 106 | if(strncmp(pkt, "have ", 5) != 0){ 107 | werrstr(" protocol garble %s", pkt); 108 | goto error; 109 | } 110 | if(hparse(&h, &pkt[5]) == -1){ 111 | werrstr(" garbled have"); 112 | goto error; 113 | } 114 | if((o = readobject(h)) == nil) 115 | continue; 116 | if(!acked){ 117 | if(fmtpkt(c, "ACK %H", h) == -1) 118 | goto error; 119 | acked = 1; 120 | } 121 | unref(o); 122 | *tail = erealloc(*tail, (*ntail + 1)*sizeof(Hash)); 123 | (*tail)[*ntail] = h; 124 | *ntail += 1; 125 | } 126 | if(!acked && fmtpkt(c, "NAK\n") == -1) 127 | goto error; 128 | return 0; 129 | error: 130 | fmtpkt(c, "ERR %r\n"); 131 | free(*head); 132 | free(*tail); 133 | return -1; 134 | } 135 | 136 | int 137 | servpack(Conn *c) 138 | { 139 | Hash *head, *tail, h; 140 | int nhead, ntail; 141 | 142 | dprint(1, "negotiating pack\n"); 143 | if(servnegotiate(c, &head, &nhead, &tail, &ntail) == -1) 144 | fail(c, "negotiate: %r"); 145 | dprint(1, "writing pack\n"); 146 | if(writepack(c->wfd, head, nhead, tail, ntail, &h) == -1) 147 | fail(c, "send: %r"); 148 | return 0; 149 | } 150 | 151 | int 152 | validref(char *s) 153 | { 154 | cleanname(s); 155 | if(strncmp(s, "refs/", 5) != 0) 156 | return 0; 157 | for(; *s != '\0'; s++) 158 | if(!isalnum(*s) && strchr("/-_.", *s) == nil) 159 | return 0; 160 | return 1; 161 | } 162 | 163 | int 164 | recvnegotiate(Conn *c, Hash **cur, Hash **upd, char ***ref, int *nupd) 165 | { 166 | char pkt[Pktmax], refpath[512], *sp[4]; 167 | Hash old, new; 168 | int n, i; 169 | 170 | if(showrefs(c) == -1) 171 | return -1; 172 | *cur = nil; 173 | *upd = nil; 174 | *ref = nil; 175 | *nupd = 0; 176 | while(1){ 177 | if((n = readpkt(c, pkt, sizeof(pkt))) == -1) 178 | goto error; 179 | if(n == 0) 180 | break; 181 | if(getfields(pkt, sp, nelem(sp), 1, " \t\n\r") != 3){ 182 | fmtpkt(c, "ERR protocol garble %s\n", pkt); 183 | goto error; 184 | } 185 | if(hparse(&old, sp[0]) == -1){ 186 | fmtpkt(c, "ERR bad old hash %s\n", sp[0]); 187 | goto error; 188 | } 189 | if(hparse(&new, sp[1]) == -1){ 190 | fmtpkt(c, "ERR bad new hash %s\n", sp[1]); 191 | goto error; 192 | } 193 | if(!validref(sp[2])){ 194 | fmtpkt(c, "ERR invalid ref %s\n", sp[2]); 195 | goto error; 196 | } 197 | *cur = erealloc(*cur, (*nupd + 1)*sizeof(Hash)); 198 | *upd = erealloc(*upd, (*nupd + 1)*sizeof(Hash)); 199 | *ref = erealloc(*ref, (*nupd + 1)*sizeof(Hash)); 200 | (*cur)[*nupd] = old; 201 | (*upd)[*nupd] = new; 202 | (*ref)[*nupd] = estrdup(sp[2]); 203 | n = snprint(refpath, sizeof(refpath), ".git/%s", sp[2]); 204 | if(n >= sizeof(refpath)-1){ 205 | fmtpkt(c, "ERR invalid ref %s\n", sp[2]); 206 | goto error; 207 | } 208 | if(access(refpath, AWRITE) == -1 209 | && access(refpath, AEXIST) == 0){ 210 | fmtpkt(c, "ERR read-only ref %s\n", sp[2]); 211 | goto error; 212 | } 213 | *nupd += 1; 214 | } 215 | return 0; 216 | error: 217 | free(*cur); 218 | free(*upd); 219 | for(i = 0; i < *nupd; i++) 220 | free((*ref)[i]); 221 | free(*ref); 222 | return -1; 223 | } 224 | 225 | int 226 | rename(char *pack, char *idx, Hash h) 227 | { 228 | char name[128], path[196]; 229 | Dir st; 230 | 231 | nulldir(&st); 232 | st.name = name; 233 | snprint(name, sizeof(name), "%H.pack", h); 234 | snprint(path, sizeof(path), ".git/objects/pack/%s", name); 235 | if(access(path, AEXIST) == 0) 236 | fprint(2, "warning, pack %s already pushed\n", name); 237 | else if(dirwstat(pack, &st) == -1) 238 | return -1; 239 | snprint(name, sizeof(name), "%H.idx", h); 240 | snprint(path, sizeof(path), ".git/objects/pack/%s", name); 241 | if(access(path, AEXIST) == 0) 242 | fprint(2, "warning, pack %s already indexed\n", name); 243 | else if(dirwstat(idx, &st) == -1) 244 | return -1; 245 | return 0; 246 | } 247 | 248 | int 249 | checkhash(int fd, vlong sz, Hash *hcomp) 250 | { 251 | DigestState *st; 252 | Hash hexpect; 253 | char buf[Pktmax]; 254 | vlong n, r; 255 | int nr; 256 | 257 | if(sz < 28){ 258 | werrstr("undersize packfile"); 259 | return -1; 260 | } 261 | 262 | st = nil; 263 | n = 0; 264 | if(seek(fd, 0, 0) == -1) 265 | sysfatal("packfile seek: %r"); 266 | while(n != sz - 20){ 267 | nr = sizeof(buf); 268 | if(sz - n - 20 < sizeof(buf)) 269 | nr = sz - n - 20; 270 | r = readn(fd, buf, nr); 271 | if(r != nr){ 272 | werrstr("short read"); 273 | return -1; 274 | } 275 | st = sha1((uchar*)buf, nr, nil, st); 276 | n += r; 277 | } 278 | sha1(nil, 0, hcomp->h, st); 279 | if(readn(fd, hexpect.h, sizeof(hexpect.h)) != sizeof(hexpect.h)) 280 | sysfatal("truncated packfile"); 281 | if(!hasheq(hcomp, &hexpect)){ 282 | werrstr("bad hash: %H != %H", *hcomp, hexpect); 283 | return -1; 284 | } 285 | return 0; 286 | } 287 | 288 | int 289 | mkdir(char *dir) 290 | { 291 | char buf[ERRMAX]; 292 | int f; 293 | 294 | if(access(dir, AEXIST) == 0) 295 | return 0; 296 | if((f = create(dir, OREAD, DMDIR | 0755)) == -1){ 297 | rerrstr(buf, sizeof(buf)); 298 | if(strstr(buf, "exist") == nil) 299 | return -1; 300 | } 301 | close(f); 302 | return 0; 303 | } 304 | 305 | int 306 | updatepack(Conn *c) 307 | { 308 | char buf[Pktmax], packtmp[128], idxtmp[128], ebuf[ERRMAX]; 309 | int n, pfd, packsz; 310 | Hash h; 311 | 312 | /* make sure the needed dirs exist */ 313 | if(mkdir(".git/objects") == -1) 314 | return -1; 315 | if(mkdir(".git/objects/pack") == -1) 316 | return -1; 317 | if(mkdir(".git/refs") == -1) 318 | return -1; 319 | if(mkdir(".git/refs/heads") == -1) 320 | return -1; 321 | snprint(packtmp, sizeof(packtmp), ".git/objects/pack/recv-%d.pack.tmp", getpid()); 322 | snprint(idxtmp, sizeof(idxtmp), ".git/objects/pack/recv-%d.idx.tmp", getpid()); 323 | if((pfd = create(packtmp, ORDWR, 0644)) == -1) 324 | return -1; 325 | packsz = 0; 326 | while(1){ 327 | n = read(c->rfd, buf, sizeof(buf)); 328 | if(n == 0) 329 | break; 330 | if(n == -1){ 331 | rerrstr(ebuf, sizeof(ebuf)); 332 | if(strstr(ebuf, "hungup") == nil) 333 | return -1; 334 | break; 335 | } 336 | if(write(pfd, buf, n) != n) 337 | return -1; 338 | packsz += n; 339 | } 340 | if(checkhash(pfd, packsz, &h) == -1){ 341 | dprint(1, "hash mismatch\n"); 342 | goto error1; 343 | } 344 | if(indexpack(packtmp, idxtmp, h) == -1){ 345 | dprint(1, "indexing failed: %r\n"); 346 | goto error1; 347 | } 348 | if(rename(packtmp, idxtmp, h) == -1){ 349 | dprint(1, "rename failed: %r\n"); 350 | goto error2; 351 | } 352 | return 0; 353 | 354 | error2: remove(idxtmp); 355 | error1: remove(packtmp); 356 | return -1; 357 | } 358 | 359 | int 360 | lockrepo(void) 361 | { 362 | int fd, i; 363 | 364 | for(i = 0; i < 10; i++) { 365 | if((fd = create(".git/_lock", ORCLOSE|ORDWR|OTRUNC|OEXCL, 0644))!= -1) 366 | return fd; 367 | sleep(250); 368 | } 369 | return -1; 370 | } 371 | 372 | int 373 | updaterefs(Conn *c, Hash *cur, Hash *upd, char **ref, int nupd) 374 | { 375 | char refpath[512], buf[128]; 376 | int i, newidx, hadref, fd, ret, lockfd; 377 | vlong newtm; 378 | Object *o; 379 | Hash h; 380 | 381 | ret = -1; 382 | hadref = 0; 383 | newidx = -1; 384 | /* 385 | * Date of Magna Carta. 386 | * Wrong because it was computed using 387 | * the proleptic gregorian calendar. 388 | */ 389 | newtm = -23811206400; 390 | if((lockfd = lockrepo()) == -1){ 391 | snprint(buf, sizeof(buf), "repo locked\n"); 392 | return -1; 393 | } 394 | for(i = 0; i < nupd; i++){ 395 | if(resolveref(&h, ref[i]) == 0){ 396 | hadref = 1; 397 | if(!hasheq(&h, &cur[i])){ 398 | snprint(buf, sizeof(buf), "old ref changed: %s", ref[i]); 399 | goto error; 400 | } 401 | } 402 | if(snprint(refpath, sizeof(refpath), ".git/%s", ref[i]) == sizeof(refpath)){ 403 | snprint(buf, sizeof(buf), "ref path too long: %s", ref[i]); 404 | goto error; 405 | } 406 | if(hasheq(&upd[i], &Zhash)){ 407 | remove(refpath); 408 | continue; 409 | } 410 | if((o = readobject(upd[i])) == nil){ 411 | snprint(buf, sizeof(buf), "update to nonexistent hash %H", upd[i]); 412 | goto error; 413 | } 414 | if(o->type != GCommit){ 415 | snprint(buf, sizeof(buf), "not commit: %H", upd[i]); 416 | goto error; 417 | } 418 | if(o->commit->mtime > newtm){ 419 | newtm = o->commit->mtime; 420 | newidx = i; 421 | } 422 | unref(o); 423 | if((fd = create(refpath, OWRITE|OTRUNC, 0644)) == -1){ 424 | snprint(buf, sizeof(buf), "open ref: %r"); 425 | goto error; 426 | } 427 | if(fprint(fd, "%H", upd[i]) == -1){ 428 | snprint(buf, sizeof(buf), "upate ref: %r"); 429 | close(fd); 430 | goto error; 431 | } 432 | close(fd); 433 | } 434 | /* 435 | * Heuristic: 436 | * If there are no valid refs, and HEAD is invalid, then 437 | * pick the ref with the newest commits as the default 438 | * branch. 439 | * 440 | * Several people have been caught out by pushing to 441 | * a repo where HEAD named differently from what got 442 | * pushed, and this is going to be more of a footgun 443 | * when 'master', 'main', and 'front' are all in active 444 | * use. This should make us pick a useful default in 445 | * those cases, instead of silently failing. 446 | */ 447 | if(resolveref(&h, "HEAD") == -1 && hadref == 0 && newidx != -1){ 448 | if((fd = create(".git/HEAD", OWRITE|OTRUNC, 0644)) == -1){ 449 | snprint(buf, sizeof(buf), "open HEAD: %r"); 450 | goto error; 451 | } 452 | if(fprint(fd, "ref: %s", ref[0]) == -1){ 453 | snprint(buf, sizeof(buf), "write HEAD ref: %r"); 454 | goto error; 455 | } 456 | close(fd); 457 | } 458 | ret = 0; 459 | error: 460 | fmtpkt(c, "ERR %s", buf); 461 | close(lockfd); 462 | werrstr(buf); 463 | return ret; 464 | } 465 | 466 | int 467 | recvpack(Conn *c) 468 | { 469 | Hash *cur, *upd; 470 | char **ref; 471 | int nupd; 472 | 473 | if(!allowwrite) 474 | fail(c, "read-only repo"); 475 | if(recvnegotiate(c, &cur, &upd, &ref, &nupd) == -1) 476 | fail(c, "negotiate refs: %r"); 477 | if(nupd != 0 && updatepack(c) == -1) 478 | sysfatal("update pack: %r"); 479 | if(nupd != 0 && updaterefs(c, cur, upd, ref, nupd) == -1) 480 | sysfatal("update refs: %r"); 481 | return 0; 482 | } 483 | 484 | char* 485 | parsecmd(char *buf, char *cmd, int ncmd) 486 | { 487 | int i; 488 | char *p; 489 | 490 | for(p = buf, i = 0; *p && i < ncmd - 1; i++, p++){ 491 | if(*p == ' ' || *p == '\t'){ 492 | cmd[i] = 0; 493 | break; 494 | } 495 | cmd[i] = *p; 496 | } 497 | while(*p == ' ' || *p == '\t') 498 | p++; 499 | return p; 500 | } 501 | 502 | void 503 | usage(void) 504 | { 505 | fprint(2, "usage: %s [-dw] [-r rel]\n", argv0); 506 | exits("usage"); 507 | } 508 | 509 | void 510 | main(int argc, char **argv) 511 | { 512 | char *repo, cmd[32], buf[512]; 513 | Conn c; 514 | 515 | ARGBEGIN{ 516 | case 'd': 517 | chattygit++; 518 | break; 519 | case 'r': 520 | pathpfx = EARGF(usage()); 521 | if(*pathpfx != '/') 522 | sysfatal("path prefix must begin with '/'"); 523 | break; 524 | case 'w': 525 | allowwrite++; 526 | break; 527 | default: 528 | usage(); 529 | break; 530 | }ARGEND; 531 | 532 | gitinit(); 533 | interactive = 0; 534 | if(rfork(RFNAMEG) == -1) 535 | sysfatal("rfork: %r"); 536 | if(pathpfx != nil){ 537 | if(bind(pathpfx, "/", MREPL) == -1) 538 | sysfatal("bind: %r"); 539 | } 540 | if(rfork(RFNOMNT) == -1) 541 | sysfatal("rfork: %r"); 542 | 543 | initconn(&c, 0, 1); 544 | if(readpkt(&c, buf, sizeof(buf)) == -1) 545 | sysfatal("readpkt: %r"); 546 | repo = parsecmd(buf, cmd, sizeof(cmd)); 547 | cleanname(repo); 548 | if(strncmp(repo, "../", 3) == 0) 549 | fail(&c, "invalid path %s\n", repo); 550 | if(bind(repo, "/", MREPL) == -1) 551 | fail(&c, "no such repo", repo); 552 | if(chdir("/") == -1) 553 | fail(&c, "no such repo"); 554 | if(access(".git", AREAD) == -1) 555 | fail(&c, "no such repo"); 556 | if(strcmp(cmd, "git-receive-pack") == 0) 557 | recvpack(&c); 558 | else if(strcmp(cmd, "git-upload-pack") == 0) 559 | servpack(&c); 560 | else 561 | fail(&c, "unsupported command '%s'", cmd); 562 | exits(nil); 563 | } 564 | -------------------------------------------------------------------------------- /util.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "git.h" 6 | 7 | Reprog *authorpat; 8 | Hash Zhash; 9 | 10 | int chattygit; 11 | int interactive = 1; 12 | 13 | enum { 14 | Seed = 2928213749ULL 15 | }; 16 | 17 | Object* 18 | emptydir(void) 19 | { 20 | static Object *e; 21 | 22 | if(e != nil) 23 | return ref(e); 24 | e = emalloc(sizeof(Object)); 25 | e->hash = Zhash; 26 | e->type = GTree; 27 | e->tree = emalloc(sizeof(Tinfo)); 28 | e->tree->ent = nil; 29 | e->tree->nent = 0; 30 | e->flag |= Cloaded|Cparsed; 31 | e->off = -1; 32 | ref(e); 33 | cache(e); 34 | return e; 35 | } 36 | 37 | int 38 | hasheq(Hash *a, Hash *b) 39 | { 40 | return memcmp(a->h, b->h, sizeof(a->h)) == 0; 41 | } 42 | 43 | static int 44 | charval(int c, int *err) 45 | { 46 | if(c >= '0' && c <= '9') 47 | return c - '0'; 48 | if(c >= 'a' && c <= 'f') 49 | return c - 'a' + 10; 50 | if(c >= 'A' && c <= 'F') 51 | return c - 'A' + 10; 52 | *err = 1; 53 | return -1; 54 | } 55 | 56 | void * 57 | emalloc(ulong n) 58 | { 59 | void *v; 60 | 61 | v = mallocz(n, 1); 62 | if(v == nil) 63 | sysfatal("malloc: %r"); 64 | setmalloctag(v, getcallerpc(&n)); 65 | return v; 66 | } 67 | 68 | void * 69 | eamalloc(ulong n, ulong sz) 70 | { 71 | uvlong na; 72 | void *v; 73 | 74 | na = (uvlong)n*(uvlong)sz; 75 | if(na >= (1ULL<<30)) 76 | sysfatal("alloc: overflow"); 77 | v = mallocz(na, 1); 78 | if(v == nil) 79 | sysfatal("malloc: %r"); 80 | setmalloctag(v, getcallerpc(&n)); 81 | return v; 82 | } 83 | 84 | void * 85 | erealloc(void *p, ulong n) 86 | { 87 | void *v; 88 | 89 | v = realloc(p, n); 90 | if(v == nil) 91 | sysfatal("realloc: %r"); 92 | setmalloctag(v, getcallerpc(&p)); 93 | return v; 94 | } 95 | 96 | void * 97 | earealloc(void *p, ulong n, ulong sz) 98 | { 99 | uvlong na; 100 | void *v; 101 | 102 | na = (uvlong)n*(uvlong)sz; 103 | if(na >= (1ULL<<30)) 104 | sysfatal("alloc: overflow"); 105 | v = realloc(p, na); 106 | if(v == nil) 107 | sysfatal("realloc: %r"); 108 | setmalloctag(v, getcallerpc(&p)); 109 | return v; 110 | } 111 | 112 | char* 113 | estrdup(char *s) 114 | { 115 | s = strdup(s); 116 | if(s == nil) 117 | sysfatal("strdup: %r"); 118 | setmalloctag(s, getcallerpc(&s)); 119 | return s; 120 | } 121 | 122 | int 123 | Hfmt(Fmt *fmt) 124 | { 125 | Hash h; 126 | int i, n, l; 127 | char c0, c1; 128 | 129 | l = 0; 130 | h = va_arg(fmt->args, Hash); 131 | for(i = 0; i < sizeof h.h; i++){ 132 | n = (h.h[i] >> 4) & 0xf; 133 | c0 = (n >= 10) ? n-10 + 'a' : n + '0'; 134 | n = h.h[i] & 0xf; 135 | c1 = (n >= 10) ? n-10 + 'a' : n + '0'; 136 | l += fmtprint(fmt, "%c%c", c0, c1); 137 | } 138 | return l; 139 | } 140 | 141 | int 142 | Tfmt(Fmt *fmt) 143 | { 144 | int t; 145 | int l; 146 | 147 | t = va_arg(fmt->args, int); 148 | switch(t){ 149 | case GNone: l = fmtprint(fmt, "none"); break; 150 | case GCommit: l = fmtprint(fmt, "commit"); break; 151 | case GTree: l = fmtprint(fmt, "tree"); break; 152 | case GBlob: l = fmtprint(fmt, "blob"); break; 153 | case GTag: l = fmtprint(fmt, "tag"); break; 154 | case GOdelta: l = fmtprint(fmt, "odelta"); break; 155 | case GRdelta: l = fmtprint(fmt, "gdelta"); break; 156 | default: l = fmtprint(fmt, "?%d?", t); break; 157 | } 158 | return l; 159 | } 160 | 161 | int 162 | Ofmt(Fmt *fmt) 163 | { 164 | Object *o; 165 | int l; 166 | 167 | o = va_arg(fmt->args, Object *); 168 | print("== %H (%T) ==\n", o->hash, o->type); 169 | switch(o->type){ 170 | case GTree: 171 | l = fmtprint(fmt, "tree\n"); 172 | break; 173 | case GBlob: 174 | l = fmtprint(fmt, "blob %s\n", o->data); 175 | break; 176 | case GCommit: 177 | l = fmtprint(fmt, "commit\n"); 178 | break; 179 | case GTag: 180 | l = fmtprint(fmt, "tag\n"); 181 | break; 182 | default: 183 | l = fmtprint(fmt, "invalid: %d\n", o->type); 184 | break; 185 | } 186 | return l; 187 | } 188 | 189 | int 190 | Qfmt(Fmt *fmt) 191 | { 192 | Qid q; 193 | 194 | q = va_arg(fmt->args, Qid); 195 | if(q.path == ~0ULL && q.vers == ~0UL && q.type == 0xff) 196 | return fmtprint(fmt, "NOQID"); 197 | else 198 | return fmtprint(fmt, "%llux.%lud.%hhx", q.path, q.vers, q.type); 199 | } 200 | 201 | void 202 | gitinit(void) 203 | { 204 | fmtinstall('H', Hfmt); 205 | fmtinstall('T', Tfmt); 206 | fmtinstall('O', Ofmt); 207 | fmtinstall('Q', Qfmt); 208 | inflateinit(); 209 | deflateinit(); 210 | authorpat = regcomp("[\t ]*(.*)[\t ]+([0-9]+)[\t ]*([\\-+]?[0-9]+)?"); 211 | osinit(&objcache); 212 | } 213 | 214 | int 215 | hparse(Hash *h, char *b) 216 | { 217 | int i, err; 218 | 219 | err = 0; 220 | for(i = 0; i < sizeof(h->h); i++){ 221 | err = 0; 222 | h->h[i] = 0; 223 | h->h[i] |= ((charval(b[2*i], &err) & 0xf) << 4); 224 | h->h[i] |= ((charval(b[2*i+1], &err)& 0xf) << 0); 225 | if(err){ 226 | werrstr("invalid hash"); 227 | return -1; 228 | } 229 | } 230 | return 0; 231 | } 232 | 233 | int 234 | slurpdir(char *p, Dir **d) 235 | { 236 | int r, f; 237 | 238 | if((f = open(p, OREAD)) == -1) 239 | return -1; 240 | r = dirreadall(f, d); 241 | close(f); 242 | return r; 243 | } 244 | 245 | int 246 | hassuffix(char *base, char *suf) 247 | { 248 | int nb, ns; 249 | 250 | nb = strlen(base); 251 | ns = strlen(suf); 252 | if(ns <= nb && strcmp(base + (nb - ns), suf) == 0) 253 | return 1; 254 | return 0; 255 | } 256 | 257 | int 258 | swapsuffix(char *dst, int dstsz, char *base, char *oldsuf, char *suf) 259 | { 260 | int bl, ol, sl, l; 261 | 262 | bl = strlen(base); 263 | ol = strlen(oldsuf); 264 | sl = strlen(suf); 265 | l = bl + sl - ol; 266 | if(l + 1 > dstsz || ol > bl) 267 | return -1; 268 | memmove(dst, base, bl - ol); 269 | memmove(dst + bl - ol, suf, sl); 270 | dst[l] = 0; 271 | return l; 272 | } 273 | 274 | char * 275 | strip(char *s) 276 | { 277 | char *e; 278 | 279 | while(isspace(*s)) 280 | s++; 281 | e = s + strlen(s); 282 | while(e > s && isspace(*--e)) 283 | *e = 0; 284 | return s; 285 | } 286 | 287 | void 288 | _dprint(char *fmt, ...) 289 | { 290 | va_list ap; 291 | 292 | va_start(ap, fmt); 293 | vfprint(2, fmt, ap); 294 | va_end(ap); 295 | } 296 | 297 | /* Finds the directory containing the git repo. */ 298 | int 299 | findrepo(char *buf, int nbuf, int *nrel) 300 | { 301 | char *p, *suff; 302 | 303 | suff = "/.git/HEAD"; 304 | if(getwd(buf, nbuf - strlen(suff) - 1) == nil) 305 | return -1; 306 | 307 | *nrel = 0; 308 | for(p = buf + strlen(buf); p != nil; p = strrchr(buf, '/')){ 309 | strcpy(p, suff); 310 | if(access(buf, AEXIST) == 0){ 311 | p[p == buf] = '\0'; 312 | return 0; 313 | } 314 | *nrel += 1; 315 | *p = '\0'; 316 | } 317 | werrstr("not a git repository"); 318 | return -1; 319 | } 320 | 321 | int 322 | showprogress(int x, int pct) 323 | { 324 | if(!interactive) 325 | return 0; 326 | if(x > pct){ 327 | pct = x; 328 | fprint(2, "\b\b\b\b%3d%%", pct); 329 | } 330 | return pct; 331 | } 332 | 333 | void 334 | qinit(Objq *q) 335 | { 336 | memset(q, 0, sizeof(Objq)); 337 | q->nheap = 0; 338 | q->heapsz = 8; 339 | q->heap = eamalloc(q->heapsz, sizeof(Qelt)); 340 | } 341 | 342 | void 343 | qclear(Objq *q) 344 | { 345 | free(q->heap); 346 | } 347 | 348 | void 349 | qput(Objq *q, Object *o, int color) 350 | { 351 | Qelt t; 352 | int i; 353 | 354 | assert(o->type == GCommit); 355 | if(q->nheap == q->heapsz){ 356 | q->heapsz *= 2; 357 | q->heap = earealloc(q->heap, q->heapsz, sizeof(Qelt)); 358 | } 359 | q->heap[q->nheap].o = o; 360 | q->heap[q->nheap].color = color; 361 | q->heap[q->nheap].ctime = o->commit->ctime; 362 | for(i = q->nheap; i > 0; i = (i-1)/2){ 363 | if(q->heap[i].ctime < q->heap[(i-1)/2].ctime) 364 | break; 365 | t = q->heap[i]; 366 | q->heap[i] = q->heap[(i-1)/2]; 367 | q->heap[(i-1)/2] = t; 368 | } 369 | q->nheap++; 370 | } 371 | 372 | int 373 | qpop(Objq *q, Qelt *e) 374 | { 375 | int i, l, r, m; 376 | Qelt t; 377 | 378 | if(q->nheap == 0) 379 | return 0; 380 | *e = q->heap[0]; 381 | if(--q->nheap == 0) 382 | return 1; 383 | 384 | i = 0; 385 | q->heap[0] = q->heap[q->nheap]; 386 | while(1){ 387 | m = i; 388 | l = 2*i+1; 389 | r = 2*i+2; 390 | if(l < q->nheap && q->heap[m].ctime < q->heap[l].ctime) 391 | m = l; 392 | if(r < q->nheap && q->heap[m].ctime < q->heap[r].ctime) 393 | m = r; 394 | if(m == i) 395 | break; 396 | t = q->heap[m]; 397 | q->heap[m] = q->heap[i]; 398 | q->heap[i] = t; 399 | i = m; 400 | } 401 | return 1; 402 | } 403 | 404 | u64int 405 | murmurhash2(void *pp, usize n) 406 | { 407 | u32int m = 0x5bd1e995; 408 | u32int r = 24; 409 | u32int h, k; 410 | u32int *w, *e; 411 | uchar *p; 412 | 413 | h = Seed ^ n; 414 | e = pp; 415 | e += (n / 4); 416 | for (w = pp; w != e; w++) { 417 | /* 418 | * NB: this is endian dependent. 419 | * This is fine for use in git, since the 420 | * hashes computed here are only ever used 421 | * for in memory data structures. 422 | * 423 | * Pack files will differ when packed on 424 | * machines with different endianness, 425 | * but the results will still be correct. 426 | */ 427 | k = *w; 428 | k *= m; 429 | k ^= k >> r; 430 | k *= m; 431 | 432 | h *= m; 433 | h ^= k; 434 | } 435 | 436 | p = (uchar*)w; 437 | switch (n & 0x3) { 438 | case 3: h ^= p[2] << 16; 439 | case 2: h ^= p[1] << 8; 440 | case 1: h ^= p[0] << 0; 441 | h *= m; 442 | } 443 | 444 | h ^= h >> 13; 445 | h *= m; 446 | h ^= h >> 15; 447 | 448 | return h; 449 | } 450 | 451 | Qid 452 | parseqid(char *s) 453 | { 454 | char *e; 455 | Qid q; 456 | 457 | if(strcmp(s, "NOQID") == 0) 458 | return (Qid){-1, -1, -1}; 459 | e = s; 460 | q.path = strtoull(e, &e, 16); 461 | if(*e != '.') 462 | sysfatal("corrupt qid: %s (%s)\n", s, e); 463 | q.vers = strtoul(e+1, &e, 10); 464 | if(*e != '.') 465 | sysfatal("corrupt qid: %s (%s)\n", s, e); 466 | q.type = strtoul(e+1, &e, 16); 467 | if(*e != '\0') 468 | sysfatal("corrupt qid: %s (%x)\n", s, *e); 469 | return q; 470 | } 471 | -------------------------------------------------------------------------------- /walk.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "git.h" 4 | 5 | typedef struct Seen Seen; 6 | typedef struct Idxed Idxed; 7 | typedef struct Idxent Idxent; 8 | 9 | #define NCACHE 4096 10 | 11 | enum { 12 | Rflg = 1 << 0, 13 | Mflg = 1 << 1, 14 | Aflg = 1 << 2, 15 | Uflg = 1 << 3, 16 | /* everything after this is not an error */ 17 | Tflg = 1 << 4, 18 | }; 19 | 20 | struct Seen { 21 | Dir* cache; 22 | int n; 23 | int max; 24 | }; 25 | 26 | struct Idxed { 27 | char** cache; 28 | int n; 29 | int max; 30 | }; 31 | 32 | Seen seentab[NCACHE]; 33 | Idxed idxtab[NCACHE]; 34 | char repopath[1024]; 35 | char wdirpath[1024]; 36 | char *rstr = "R "; 37 | char *mstr = "M "; 38 | char *astr = "A "; 39 | char *ustr = "U "; 40 | char *tstr = "T "; 41 | char *bdir = ".git/fs/HEAD/tree"; 42 | int useidx = 1; 43 | int nrel; 44 | int quiet; 45 | int dirty; 46 | int printflg; 47 | 48 | Idxent *idx; 49 | int idxsz; 50 | int nidx; 51 | int staleidx; 52 | Idxent *wdir; 53 | int wdirsz; 54 | int nwdir; 55 | 56 | int loadwdir(char*); 57 | 58 | int 59 | seen(Dir *dir) 60 | { 61 | Seen *c; 62 | Dir *dp; 63 | int i; 64 | 65 | c = &seentab[dir->qid.path&(NCACHE-1)]; 66 | dp = c->cache; 67 | for(i=0; in; i++, dp++) 68 | if(dir->qid.path == dp->qid.path 69 | && dir->qid.type == dp->qid.type 70 | && dir->dev == dp->dev) 71 | return 1; 72 | if(c->n == c->max){ 73 | if (c->max == 0) 74 | c->max = 8; 75 | else 76 | c->max += c->max/2; 77 | c->cache = realloc(c->cache, c->max*sizeof(Dir)); 78 | if(c->cache == nil) 79 | sysfatal("realloc: %r"); 80 | } 81 | c->cache[c->n++] = *dir; 82 | return 0; 83 | } 84 | 85 | int 86 | checkedin(Idxent *e, int change) 87 | { 88 | char *p; 89 | int r; 90 | 91 | p = smprint("%s/%s", bdir, e->path); 92 | r = access(p, AEXIST); 93 | if(r == 0 && change){ 94 | if(e->state != 'R') 95 | e->state = 'T'; 96 | staleidx = 1; 97 | } 98 | free(p); 99 | return r == 0; 100 | } 101 | 102 | int 103 | indexed(char *path, int isdir) 104 | { 105 | int lo, hi, mid, n, r; 106 | char *s; 107 | 108 | if(!useidx){ 109 | s = smprint("%s/%s", bdir, path); 110 | r = access(s, AEXIST); 111 | free(s); 112 | return r == 0; 113 | } 114 | s = path; 115 | if(isdir) 116 | s = smprint("%s/", path); 117 | r = -1; 118 | lo = 0; 119 | hi = nidx-1; 120 | n = strlen(s); 121 | while(lo <= hi){ 122 | mid = (hi + lo) / 2; 123 | if(isdir) 124 | r = strncmp(s, idx[mid].path, n); 125 | else 126 | r = strcmp(s, idx[mid].path); 127 | if(r < 0) 128 | hi = mid-1; 129 | else if(r > 0) 130 | lo = mid+1; 131 | else 132 | break; 133 | } 134 | if(isdir) 135 | free(s); 136 | return r == 0; 137 | } 138 | 139 | int 140 | idxcmp(void *pa, void *pb) 141 | { 142 | Idxent *a, *b; 143 | int c; 144 | 145 | a = (Idxent*)pa; 146 | b = (Idxent*)pb; 147 | if((c = strcmp(a->path, b->path)) != 0) 148 | return c; 149 | /* order is unique */ 150 | return a-> order < b->order ? -1 : 1; 151 | } 152 | 153 | /* 154 | * compares whether the indexed entry 'a' 155 | * has the same contents and mode as 156 | * the entry on disk 'b'; if the indexed 157 | * entry is nil, does a deep comparison 158 | * of the checked out file and the file 159 | * checked in. 160 | */ 161 | int 162 | samedata(Idxent *a, Idxent *b) 163 | { 164 | char *gitpath, ba[IOUNIT], bb[IOUNIT]; 165 | int fa, fb, na, nb, same; 166 | Dir *da, *db; 167 | 168 | if(a != nil){ 169 | if(a->qid.path == b->qid.path 170 | && a->qid.vers == b->qid.vers 171 | && a->qid.type == b->qid.type 172 | && a->mode == b->mode 173 | && a->mode != 0) 174 | return 1; 175 | } 176 | 177 | same = 0; 178 | da = nil; 179 | db = nil; 180 | if((gitpath = smprint("%s/%s", bdir, b->path)) == nil) 181 | sysfatal("smprint: %r"); 182 | fa = open(gitpath, OREAD); 183 | fb = open(b->path, OREAD); 184 | if(fa == -1 || fb == -1) 185 | goto mismatch; 186 | da = dirfstat(fa); 187 | db = dirfstat(fb); 188 | if(da == nil || db == nil) 189 | goto mismatch; 190 | if((da->mode&0100) != (db->mode&0100)) 191 | goto mismatch; 192 | if(da->length != db->length) 193 | goto mismatch; 194 | while(1){ 195 | if((na = readn(fa, ba, sizeof(ba))) == -1) 196 | goto mismatch; 197 | if((nb = readn(fb, bb, sizeof(bb))) == -1) 198 | goto mismatch; 199 | if(na != nb) 200 | goto mismatch; 201 | if(na == 0) 202 | break; 203 | if(memcmp(ba, bb, na) != 0) 204 | goto mismatch; 205 | } 206 | if(a != nil){ 207 | a->qid = db->qid; 208 | a->mode = db->mode; 209 | staleidx = 1; 210 | } 211 | same = 1; 212 | 213 | mismatch: 214 | free(da); 215 | free(db); 216 | if(fa != -1) 217 | close(fa); 218 | if(fb != -1) 219 | close(fb); 220 | return same; 221 | } 222 | 223 | int 224 | loadent(char *dir, Dir *d, int fullpath) 225 | { 226 | char *path; 227 | int ret, isdir; 228 | Idxent *e; 229 | 230 | if(fullpath) 231 | path = strdup(dir); 232 | else 233 | path = smprint("%s/%s", dir, d->name); 234 | if(path == nil) 235 | sysfatal("smprint: %r"); 236 | 237 | cleanname(path); 238 | if(strncmp(path, ".git/", 5) == 0){ 239 | free(path); 240 | return 0; 241 | } 242 | ret = 0; 243 | isdir = d->qid.type & QTDIR; 244 | if((printflg & Uflg) == 0 && !indexed(path, isdir)){ 245 | free(path); 246 | return 0; 247 | } 248 | if(isdir){ 249 | ret = loadwdir(path); 250 | free(path); 251 | }else{ 252 | if(nwdir == wdirsz){ 253 | wdirsz += wdirsz/2; 254 | wdir = erealloc(wdir, wdirsz*sizeof(Idxent)); 255 | } 256 | e = wdir + nwdir; 257 | e->path = path; 258 | e->qid = d->qid; 259 | e->mode = d->mode; 260 | e->order = nwdir; 261 | e->state = 'T'; 262 | nwdir++; 263 | } 264 | return ret; 265 | } 266 | 267 | int 268 | loadwdir(char *path) 269 | { 270 | int fd, ret, i, n; 271 | Dir *d, *e; 272 | 273 | d = nil; 274 | e = nil; 275 | ret = -1; 276 | cleanname(path); 277 | if(strncmp(path, ".git/", 5) == 0) 278 | return 0; 279 | if((fd = open(path, OREAD)) < 0) 280 | goto error; 281 | if((e = dirfstat(fd)) == nil) 282 | sysfatal("fstat: %r"); 283 | if(e->qid.type & QTDIR) 284 | while((n = dirread(fd, &d)) > 0){ 285 | for(i = 0; i < n; i++) 286 | if(loadent(path, &d[i], 0) == -1) 287 | goto error; 288 | free(d); 289 | } 290 | else{ 291 | if(loadent(path, e, 1) == -1) 292 | goto error; 293 | } 294 | ret = 0; 295 | error: 296 | free(e); 297 | if(fd != -1) 298 | close(fd); 299 | return ret; 300 | } 301 | 302 | int 303 | pfxmatch(char *p, char **pfx, int *pfxlen, int npfx) 304 | { 305 | int i; 306 | 307 | if(p == nil) 308 | return 0; 309 | if(npfx == 0) 310 | return 1; 311 | for(i = 0; i < npfx; i++){ 312 | if(strncmp(p, pfx[i], pfxlen[i]) != 0) 313 | continue; 314 | if(p[pfxlen[i]] == '/' || p[pfxlen[i]] == 0) 315 | return 1; 316 | if(strcmp(pfx[i], ".") == 0 || *pfx[i] == 0) 317 | return 1; 318 | } 319 | return 0; 320 | } 321 | 322 | 323 | char* 324 | reporel(char *s) 325 | { 326 | char *p; 327 | int n; 328 | 329 | if(*s == '/') 330 | s = strdup(s); 331 | else 332 | s = smprint("%s/%s", wdirpath, s); 333 | p = cleanname(s); 334 | n = strlen(repopath); 335 | if(strncmp(s, repopath, n) != 0) 336 | sysfatal("path outside repo: %s", s); 337 | p += n; 338 | if(*p == '/') 339 | p++; 340 | memmove(s, p, strlen(p)+1); 341 | return s; 342 | } 343 | 344 | void 345 | show(Biobuf *o, int flg, char *str, char *path) 346 | { 347 | dirty |= flg; 348 | if(!quiet && (printflg & flg)) 349 | Bprint(o, "%s%s\n", str, path); 350 | } 351 | 352 | void 353 | usage(void) 354 | { 355 | fprint(2, "usage: %s [-qbc] [-f filt] [-b base] [paths...]\n", argv0); 356 | exits("usage"); 357 | } 358 | 359 | void 360 | main(int argc, char **argv) 361 | { 362 | char *p, *e, *ln, *base, **argrel, *parts[4], xbuf[8]; 363 | int i, j, c, line, wfd, *argn; 364 | Biobuf *f, *o, *w; 365 | Hash h, hd; 366 | Dir rn; 367 | 368 | gitinit(); 369 | if(access(".git/fs/ctl", AEXIST) != 0) 370 | sysfatal("no running git/fs"); 371 | if(getwd(wdirpath, sizeof(wdirpath)) == nil) 372 | sysfatal("getwd: %r"); 373 | if(findrepo(repopath, sizeof(repopath), &nrel) == -1) 374 | sysfatal("find root: %r"); 375 | if(chdir(repopath) == -1) 376 | sysfatal("chdir: %r"); 377 | 378 | ARGBEGIN{ 379 | case 'q': 380 | quiet++; 381 | break; 382 | case 'c': 383 | rstr = ""; 384 | tstr = ""; 385 | mstr = ""; 386 | astr = ""; 387 | ustr = ""; 388 | break; 389 | case 'f': 390 | for(p = EARGF(usage()); *p; p++) 391 | switch(*p){ 392 | case 'T': printflg |= Tflg; break; 393 | case 'A': printflg |= Aflg; break; 394 | case 'M': printflg |= Mflg; break; 395 | case 'R': printflg |= Rflg; break; 396 | case 'U': printflg |= Uflg; break; 397 | default: usage(); break; 398 | } 399 | break; 400 | case 'b': 401 | useidx = 0; 402 | base = EARGF(usage()); 403 | if(resolveref(&h, base) == -1) 404 | sysfatal("no such ref '%s'", base); 405 | /* optimization: we're a lot faster when using the index */ 406 | if(resolveref(&hd, "HEAD") == 0 && hasheq(&h, &hd)) 407 | useidx = 1; 408 | bdir = smprint(".git/fs/object/%H/tree", h); 409 | break; 410 | default: 411 | usage(); 412 | }ARGEND; 413 | 414 | if(printflg == 0) 415 | printflg = Tflg | Aflg | Mflg | Rflg; 416 | 417 | nidx = 0; 418 | idxsz = 32; 419 | idx = emalloc(idxsz*sizeof(Idxent)); 420 | nwdir = 0; 421 | wdirsz = 32; 422 | wdir = emalloc(wdirsz*sizeof(Idxent)); 423 | argrel = emalloc(argc*sizeof(char*)); 424 | argn = emalloc(argc*sizeof(int)); 425 | for(i = 0; i < argc; i++){ 426 | argrel[i] = reporel(argv[i]); 427 | argn[i] = strlen(argrel[i]); 428 | } 429 | if((o = Bfdopen(1, OWRITE)) == nil) 430 | sysfatal("open out: %r"); 431 | if(useidx){ 432 | if((f = Bopen(".git/INDEX9", OREAD)) == nil){ 433 | fprint(2, "open index: %r\n"); 434 | if(access(".git/index9", AEXIST) == 0){ 435 | fprint(2, "index format conversion needed:\n"); 436 | fprint(2, "\tcd %s && git/fs\n", repopath); 437 | fprint(2, "\t@{cd .git/index9/removed >[2]/dev/null && walk -f | sed 's/^/R NOQID 0 /'} >> .git/INDEX9\n"); 438 | fprint(2, "\t@{cd .git/fs/HEAD/tree && walk -f | sed 's/^/T NOQID 0 /'} >> .git/INDEX9\n"); 439 | } 440 | exits("noindex"); 441 | } 442 | line = 0; 443 | while((ln = Brdstr(f, '\n', 1)) != nil){ 444 | line++; 445 | /* allow blank lines */ 446 | if(ln[0] == 0 || ln[0] == '\n') 447 | continue; 448 | if(getfields(ln, parts, nelem(parts), 0, " \t") != nelem(parts)) 449 | sysfatal(".git/INDEX9:%d: corrupt index", line); 450 | if(nidx == idxsz){ 451 | idxsz += idxsz/2; 452 | idx = realloc(idx, idxsz*sizeof(Idxent)); 453 | } 454 | cleanname(parts[3]); 455 | if(strncmp(parts[3], ".git/", 5) == 0){ 456 | staleidx = 1; 457 | free(ln); 458 | continue; 459 | } 460 | idx[nidx].state = *parts[0]; 461 | idx[nidx].qid = parseqid(parts[1]); 462 | idx[nidx].mode = strtol(parts[2], nil, 8); 463 | idx[nidx].path = strdup(parts[3]); 464 | idx[nidx].order = nidx; 465 | nidx++; 466 | free(ln); 467 | } 468 | qsort(idx, nidx, sizeof(Idxent), idxcmp); 469 | } 470 | 471 | for(i = 0; i < argc; i++){ 472 | argrel[i] = reporel(argv[i]); 473 | argn[i] = strlen(argrel[i]); 474 | } 475 | if(argc == 0) 476 | loadwdir("."); 477 | else for(i = 0; i < argc; i++) 478 | loadwdir(argrel[i]); 479 | qsort(wdir, nwdir, sizeof(Idxent), idxcmp); 480 | for(i = 0; i < argc; i++){ 481 | argrel[i] = reporel(argv[i]); 482 | argn[i] = strlen(argrel[i]); 483 | } 484 | i = 0; 485 | j = 0; 486 | while(i < nidx || j < nwdir){ 487 | /* find the last entry we tracked for a path */ 488 | while(i+1 < nidx && strcmp(idx[i].path, idx[i+1].path) == 0){ 489 | staleidx = 1; 490 | i++; 491 | } 492 | while(j+1 < nwdir && strcmp(wdir[j].path, wdir[j+1].path) == 0) 493 | j++; 494 | if(i < nidx && !pfxmatch(idx[i].path, argrel, argn, argc)){ 495 | i++; 496 | continue; 497 | } 498 | if(i >= nidx) 499 | c = 1; 500 | else if(j >= nwdir) 501 | c = -1; 502 | else 503 | c = strcmp(idx[i].path, wdir[j].path); 504 | /* exists in both index and on disk */ 505 | if(c == 0){ 506 | if(idx[i].state == 'R'){ 507 | if(checkedin(&idx[i], 0)) 508 | show(o, Rflg, rstr, idx[i].path); 509 | else{ 510 | idx[i].state = 'U'; 511 | staleidx = 1; 512 | } 513 | }else if(idx[i].state == 'A' && !checkedin(&idx[i], 1)) 514 | show(o, Aflg, astr, idx[i].path); 515 | else if(!samedata(&idx[i], &wdir[j])) 516 | show(o, Mflg, mstr, idx[i].path); 517 | else 518 | show(o, Tflg, tstr, idx[i].path); 519 | i++; 520 | j++; 521 | /* only exists in index */ 522 | }else if(c < 0){ 523 | if(checkedin(&idx[i], 0)) 524 | show(o, Rflg, rstr, idx[i].path); 525 | i++; 526 | /* only exists on disk */ 527 | }else{ 528 | if(!useidx && checkedin(&wdir[j], 0)){ 529 | if(samedata(nil, &wdir[j])) 530 | show(o, Tflg, tstr, wdir[j].path); 531 | else 532 | show(o, Mflg, mstr, wdir[j].path); 533 | }else if(printflg & Uflg && pfxmatch(idx[i].path, argrel, argn, argc)) 534 | show(o, Uflg, ustr, wdir[j].path); 535 | j++; 536 | } 537 | } 538 | Bterm(o); 539 | 540 | if(useidx && staleidx) 541 | if((wfd = create(".git/INDEX9.new", OWRITE, 0644)) != -1){ 542 | if((w = Bfdopen(wfd, OWRITE)) == nil){ 543 | close(wfd); 544 | goto Nope; 545 | } 546 | for(i = 0; i < nidx; i++){ 547 | while(i+1 < nidx && strcmp(idx[i].path, idx[i+1].path) == 0) 548 | i++; 549 | if(idx[i].state == 'U') 550 | continue; 551 | Bprint(w, "%c %Q %o %s\n", 552 | idx[i].state, 553 | idx[i].qid, 554 | idx[i].mode, 555 | idx[i].path); 556 | } 557 | Bterm(w); 558 | nulldir(&rn); 559 | rn.name = "INDEX9"; 560 | if(remove(".git/INDEX9") == -1) 561 | goto Nope; 562 | if(dirwstat(".git/INDEX9.new", &rn) == -1) 563 | sysfatal("rename: %r"); 564 | } 565 | 566 | Nope: 567 | if(!dirty) 568 | exits(nil); 569 | 570 | p = xbuf; 571 | e = p + sizeof(xbuf); 572 | for(i = 0; (1 << i) != Tflg; i++) 573 | if(dirty & (1 << i)) 574 | p = seprint(p, e, "%c", "RMAUT"[i]); 575 | exits(xbuf); 576 | } 577 | --------------------------------------------------------------------------------