diff --git a/sys/lib/git/common.rc b/sys/lib/git/common.rc new file mode 100644 index 000000000..653d6bfb1 --- /dev/null +++ b/sys/lib/git/common.rc @@ -0,0 +1,109 @@ +nl=' +' + +fn die{ + >[1=2] echo $0: $* + exit $"* +} + +fn usage{ + >[1=2] echo -n 'usage:' $usage + exit 'usage' +} + +fn subst { + awk ' + BEGIN{ARGC=0} + {sub(ARGV[1], ARGV[2]); print} + ' $* +} + +fn drop { + awk ' + BEGIN{ARGC=0} + { + if(index($0, ARGV[1]) == 1) + $0=substr($0, length(ARGV[1])+1) + print + } + ' $* +} + +fn present { + if(~ $1 /dev/null && cmp $2 $3>/dev/null) + status=gone + if not if (~ $3 /dev/null && cmp $1 $2>/dev/null) + status=gone + if not + status=() +} + +fn whoami{ + name=`$nl{git/conf user.name} + email=`$nl{git/conf user.email} + if(test -f /adm/keys.who){ + if(~ $name '') + name=`$nl{awk -F'|' '$1=="'$user'" {x=$3} END{print x}' $tmp) + echo merge needed: $out >[1=2] + + if(present $ours $base $theirs){ + mv $tmp $out + git/add $out + } + if not { + rm -f $tmp $out + git/rm $out + } +}} + +fn gitup{ + gitroot=`{git/conf -r >[2]/dev/null} + if(~ $#gitroot 0) + die 'not a git repository' + gitfs=$gitroot/.git/fs + gitrel=`{pwd | drop $gitroot | sed 's@^/@@'} + if(~ $#gitrel 0) + gitrel='.' + cd $gitroot + startfs=() + if(! test -d $gitfs) + mkdir -p $gitfs + if(! test -e $gitfs/ctl) + startfs=true + if(! grep -s '^repo '$gitroot'$' $gitfs/ctl >[2]/dev/null) + startfs=true + if(~ $#startfs 1) + git/fs + if not + status='' +} diff --git a/sys/man/1/git b/sys/man/1/git new file mode 100644 index 000000000..5f1e7cef6 --- /dev/null +++ b/sys/man/1/git @@ -0,0 +1,654 @@ +.TH GIT 1 +.SH NAME +git, git/conf, git/query, git/walk, git/clone, git/branch, +git/commit, git/diff, git/init, git/log, git/merge, git/push, +git/pull, git/rm, git/serve +\- Manage git repositories. + +.SH SYNOPSIS +.PP +.B git/add +[ +.B -r +] +.I path... +.PP +.B git/branch +[ +.B -admns +] +[ +.B -b +.I base +] +.I newbranch +.PP +.B git/clone +[ +.I remote +[ +.I local +] +] +.PP +.B git/commit +[ +.B -re +] +[ +.B -m msg +] +[ +.I file... +] +.PP +.B git/compat +.PP +.B git/conf +[ +.B -r +] +[ +.B -f +.I file +] +.I keys... +.PP +.B git/diff +[ +.B -c +.I branch +] +[ +.B -s +] +[ +.I file... +] +.PP +.B git/export +[ +.I commits... +] +.PP +.B git/import +[ +.I commits... +] +.PP +.B git/init +[ +.B -b +] +[ +.I dir +] +[ +.B -u +.I upstream +] +.PP +.B git/log +[ +.B -c +.I commit +.B | -e +.I expr +] +[ +.B -s +] +[ +.I files... +] +.PP +.B git/merge +.I theirs +.PP +.B git/rebase +[ +.B -ari +] +[ +.B onto +] +.PP +.B git/pull +[ +.B -fq +] +[ +.B -u +.I upstream +] +.PP +.B git/push +[ +.B -af +] +[ +.B -u +.I upstream +] +[ +.B -b +.I branch +] +[ +.B -r +.I branch +] +.PP +.B git/query +[ +.B -pcr +] +.I query +.PP +.B git/revert +[ +.B -c +.I commit +] +.I file... +.PP +.B git/rm +.I path... +.PP +.B git/serve +[ +.B -w +] +[ +.B -r +.I path +] +.PP +.B git/walk +[ +.B -qc +] +[ +.B -b +.I branch +] +[ +.B -f +.I filters +] +[ +.I [files...] +] + +.SH DESCRIPTION +.PP +Git is a distributed version control system. +This means that each repository contains a full copy of the history. +This history is then synced between computers as needed. + +.PP +These programs provide tools to manage and interoperate with +repositories hosted in git. + +.SH CONCEPTS + +Git stores snapshots of the working directory. +Files can either be in a tracked or untracked state. +Each commit takes the current version of all tracked files and +adds them to a new commit. + +This history is stored in the +.I .git +directory. +This suite of +.I git +tools provides a file interface to the +.I .git +directory mounted on +.I $repo/.git/fs. +Modifications to the repository are done directly to the +.I .git +directory, and are reflected in the file system interface. +This allows for easy scripting, without excessive complexity +in the file API. + +.SH COMMANDS + +.PP +.B Git/init +is used to create a new git repository, with no code or commits. +The repository is created in the current directory by default. +Passing a directory name will cause the repository to be created +there instead. +Passing the +.B -b +option will cause the repository to be initialized as a bare repository. +Passing the +.B -u +.I upstream +option will cause the upstream to be configured to +.I upstream. + +.PP +.B Git/clone +will take an existing repository, served over either the +.I git:// +or +.I ssh:// +protocols. +The first argument is the repository to clone. +The second argument, optionally, specifies the location to clone into. +If not specified, the repository will be cloned into the last path component +of the clone source, with the +.I .git +stripped off if present. + +.PP +.B Git/push +is used to push the current changes to a remote repository. +When no arguments are provided, the remote repository is taken from +the origin configured in +.I .git/config, +and only the changes on the current branch are pushed. +When passed the +.I -a +option, all branches are pushed. +When passed the +.I -u upstream +option, the changes are pushed to +.I upstream +instead of the configured origin. +When given the +.I -r +option, the branch is deleted from origin, instead of updated. + +.PP +.B Git/revert +restores the named files from HEAD. When passed the -c flag, restores files from +the named commit. + +.PP +.B Git/pull +behaves in a similar manner to git/push, however it gets changes from +the upstream repository. +After fetching, it checks out the changes into the working directory. +When passed the +.I -f +option, the update of the working copy is suppressed. +When passed the +.I -q +option, the listing of changes is silenced. +When passed the +.I -u upstream +option, the changes are pulled from +.I upstream +instead of the configured origin. +when passed the +.I -b branch +option, it only pulls changes related to +.IR branch . + +.PP +.B Git/serve +serves repositories using the +.I git:// +protocol over stdin. +By default, it serves them read-only. +The +.I -w +flag, it allows pushing into repositories. +The +.I -r +.B path +flag serves repositories relative to +.BR path . + +.PP +.B Git/fs +serves a file system on $repo/.git/fs. +For full documentation, see +.IR gitfs (4) + +.PP +.B Git/add +adds a file to the list of tracked files. When passed the +.I -r +flag, the file is removed from the list of tracked files. +The copy of the file in the repository is left untouched. +.PP +.B Git/rm +is an alias for +.IR git/add\ -r . + +.PP +.B Git/commit +creates a new commit consisting of all changes to the specified files. +By default, an editor is opened to prepare the commit message. +The +.I -m +flag supplies the commit message directly. +The +.I -r +flag revises the contents of the previous commit, reusing the message. +The +.I -e +flag opens an editor to finalize the commit message, regardless of +whether or not it was specified explicitly or reused. +To amend a commit message, +.I -r +can be used in conjuction with +.I -m +or +.IR -e . + +.PP +.B Git/branch +is used to list or switch branches. +When invoked with no arguments, it lists the current branch. +To list all branches, pass the +.I -a +option. +To switch between branches, pass a branch name. +When passed the +.I -n +option, the branch will be created, overwriting existing branch. +When passed the +.I -b base +option, the branch created is based off of +.I base +instead of +.I HEAD. +When passed the +.I -s +option, the branch is created but the files are not checked out. +When passed the +.I -d +option, the branch is deleted. +.PP +When switching branches, git/branch will refuse to clobber +modificiations. +Passing the +.I -m +option will cause git9 to attempt to merge the changes between +the branches. + +.PP +.B Git/log +shows a history of the current branch. +When passed a list of files, only commits affecting +those files are shown. +The +.I -c commit +option logs starting from the provided commit, instead of HEAD. +The +.I -s +option shows a summary of the commit, instead of the full message. +The +.I -e expr +option shows commits matching the query expression provided. +The expression is in the syntax of +.B git/query. + +.PP +.B Git/diff +shows the differences between the currently checked out code and +the +.I HEAD +commit. +When passed the +.I -c base +option, the diff is computed against +.I base +instead of +.I HEAD. +When passed the +.I -s +option, only the file statuses are +printed. + +.PP +.B Git/export +exports a list of commits in a format that +.B git/import +can apply. + +.PP +.B Git/import +imports a commit with message, author, and +date information. + +.PP +.B Git/merge +takes two branches and merges them filewise using +.I ape/diff3. +The next commit made will be a merge commmit. + +.PP +.B Git/rebase +takes one branch and moves it onto another. +On error, the remaining commits to rebase are +saved, and can be resumed once the conflict is +resolved using the +.I -r +option. +If the rebase is to be aborted, the +.I -a +option will clean up the in progress rebase +and reset the state of the branch. +The +.I -i +option will open an editor to modify the todo-list before the rebase +begins. + +.PP +The following rebase commands are supported: +.TP 10 +.B pick +Apply the commit. +.TP +.B reword +Apply the commit, then edit its commit message. +.TP +.B edit +Apply the commit, then exit to allow further changes. +.TP +.B squash +Fold the commit into the previous commit, then edit the combined +commit message. +.TP +.B fixup +Fold the commit into the previous commit, discarding its commit +message. +.TP +.B break +Exit to allow for manual edits or inspection before continuing. + +.PP +.B Git/conf +is a tool for querying the git configuration. +The configuration key is provided as a dotted string. Spaces +are accepted. For example, to find the URL of the origin +repository, one might pass +.I 'remote\ "origin".url'. +When given the +.I -r +option, the root of the current repository is printed. + +.B Git/query +takes an expression describing a commit, or set of commits, +and resolves it to a list of commits. +The +.I -r +option reverses the order of the commit list. +With the +.I -p +option, instead of printing the commit hashes, the full +path to their +.B git/fs +path is printed. With the +.I -c +option, the query must resolve to two commits. The blobs +that have changed in the commits are printed. + +.PP +.B Git/walk +provides a tool for walking the list of tracked objects and printing their status. +With no arguments, it prints a list of paths prefixed with the status character. +When given the +.I -c +character, only the paths are printed. +When given the +.I -q +option, all output is suppressed, and only the status is printed. +When given the +.I -f +option, the output is filtered by status code, and only matching items are printed. + +.PP +The status characters are as follows: +.TP +T +Tracked, not modified since last commit. +.TP +M +Modified since last commit. +.TP +R +This file will be gone in the next commit. +.TP +A +This file will be present in the next commit. + +.PP +.B Git/compat +spawns an rc subshell with a compatibility stub in +.IR $path . +This compatibility stub provides enough of the unix +.I git +commands to run tools like +.I go get +but not much more. + +.SH REF SYNTAX + +.PP +Refs are specified with a simple query syntax. +A bare hash always evaluates to itself. +Ref names are resolved to their hashes. +The +.B a ^ +suffix operator finds the parent of a commit. +The +.B a b @ +suffix operator finds the common ancestor of the previous two commits. +The +.B a .. b +or +.B a : b +operator finds all commits between +.B a +and +.B b. +Between is defined as the set of all commits which are reachable from +.B b +but not reachable from +.B a. + +.SH PROTOCOLS +.PP +Git9 supports URL schemes of the format +.BR transport://dial/repo/path . +The transport portion specifies the protocol to use. +If the transport portion is omitted, then the transport used is +.BR ssh . +The +.I dial +portion is either a plan 9 dial string, or a conventional +.I host:port +pair. +For the ssh protocol, it may also include a +.I user@ +prefix. +.I repo/path +portion is the path of the repository on the server. + +The supported transports are +.B ssh://, git://, hjgit://, gits://, http://, +and +.BR https . +Two of these are specific to git9: +.I gits:// +and +.IR hjgit:// . +Both are the +.I git:// +protocol, tunnelled over tls. +.I Hjgit:// +authenticates with the server using Plan 9 authentication, +using +.IR tlsclient\ -a . +Any of these protocol names may be prefixed with +.IR git+ , +for copy-paste compatibility with Unix git. + +.SH EXAMPLES + +.PP +In order to create a new repository, run +.B git/init: +.PP +.EX +git/init myrepo +.EE + +.PP +To clone an existing repository from a git server, run: +.PP +.EX +git/clone git://github.com/Harvey-OS/harvey +cd harvey +# edit files +git/commit foo.c +git/push +.EE + +.PP +To set a user and email for commits, run: +.PP +.EX +% mkdir $home/lib/git +% >$home/lib/git/config echo ' +[user] + name = Ori Bernstein + email = ori@eigenstate.org' +.EE + +.SH FILES +.TP +$repo/.git +The full git repository. +.TP +$repo/.git/config +The configuration file for a repository. +.TP +$home/lib/git/config +The user-wide configuration for git. +The contents of this file are used as fallbacks for the per-repository config. +.TP +/sys/lib/git/config +The system-wide configuration for git. +The contents of this file are used as fallbacks for the per-user config. + +.SH SEE ALSO +.IR replica (1), +.IR patch (1), +.IR gitfs (4), +.I diff3 + +.SH BUGS +.PP +Repositories with submodules are effectively read-only. +.PP +There are some missing commands, features, and tools. +.PP +git/compat only works within a git repository. diff --git a/sys/man/4/gitfs b/sys/man/4/gitfs new file mode 100644 index 000000000..7ba097ad0 --- /dev/null +++ b/sys/man/4/gitfs @@ -0,0 +1,112 @@ +.TH GITFS 4 +.SH NAME +git/fs \- git file server + +.SH SYNOPSIS + +git/fs +[ +.B -d +] +[ +.B -m +.I mtpt +] + +.SH DESCRIPTION + +.PP +Git/fs serves a file system interface to a git repository in the +current directory. +This file system provides a read-only view into the repository contents. +By default, it is mounted on +.B $repo/.git/fs. +It does not cache mutable data, so any changes to the git repository will immediately be reflected in git/fs. + +.PP +Git/fs serves a few levels of hierarchy. +The top level contains the following files and directories: + +.TP +.B branch +Exposes branches. Branches are aliases for commit objects. + +.TP +.B object +Exposes all objects in the git repository. +Objects may be commits, trees, or blobs. + +.TP +.B HEAD +This is an alias for the current commit. + +.PP +Commits are also represented as small hierarchies. They contain +the following files: + +.TP +.B author +This is the author of the commit. +The contents of this file are free-form text, but conventionally +they take the form +.B Full Name + +.TP +.B hash +The commit id of the current branch + +.TP +.B msg +The full text of the commit message. + +.TP +.B parent +The list of parent commit ids of the current commit. +One parent is listed per line. + +.TP +.B tree +A directory containing the tree associated with the +commit. +The timestamp of the files contained within this +hierarchy are the same as the date of the commit. + +.PP +Trees are presented as directory listings, and blobs +as files. + +.SH FILES +.TP +.B .git +The git repository being expected. +.TP +.B .git/HEAD +A reference to the current HEAD. +Used to populate +.B $repo/.git/fs/HEAD +.TP +.git/config +The per-repository configuation for git tools. +.TP +.B $home/lib/git/config +The global configuration for git tools. + +.SH SOURCE +.TP +.B /sys/src/cmd/git/fs.c + +.SH "SEE ALSO" +.IR git (1) +.IR hg (1) +.IR hgfs (4) + +.SH BUGS +Symlinks are only partially supported. +Symlinks are treated as regular files when reading. +Modifying symlinks is unsupported. + +.PP +There is no way to inspect the raw objects. This is +a feature that would be useful for debugging. + + diff --git a/sys/src/cmd/git/LICENSE b/sys/src/cmd/git/LICENSE new file mode 100644 index 000000000..b8852f1a3 --- /dev/null +++ b/sys/src/cmd/git/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019 Ori Bernstein + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sys/src/cmd/git/README b/sys/src/cmd/git/README new file mode 100644 index 000000000..695d761cd --- /dev/null +++ b/sys/src/cmd/git/README @@ -0,0 +1,198 @@ +Git for Plan 9: git/fs +====================== + +All commits in this repository were made with git9 running on 9front. + +About +----- + +Plan 9 is a non-posix system. Upstream git has been ported, but feels +distinctly un-plan9ish, and even in its native environment, has been +justifiably tarred and feathered for its user experience. + +Git/fs implements a git client for plan 9. The intent is to support +working with git repositories, without cloning the git interface +directly. + +Git/fs is my daily driver. It's solid, and works well for my needs. + +Getting Started +--------------- + +** USE THE VERSION THAT COMES WITH 9FRONT ** + +The version that comes with 9front is the most up to date +version out there. If you are on 9front, you do not need +this repository. + +If you are *NOT* on 9front: + +Install the appropriate patches to your system (listed below), +then: + +Get a bootstrap copy: + + % cd /tmp + % hget https://orib.dev/git/git9/HEAD/snap.tar.gz | tar xvz + % cd git9 + % mk all + % mk install + +Then get an updatable version from git: + + % cd $home/src + % git/clone gits://orib.dev/git9 + % cd git9 + % mk all + % mk install + +Configure your user name and email: + + % mkdir -p $home/lib/git + % echo ' + [user] + name=Ori Bernstein + email=ori@eigenstate.org + ' > $home/lib/git/config + +Read the docs: + + % man 1 git + % man 4 gitfs + +And start committing! + +Structure +--------- + +The git/fs program provides a file system mounted on $repo/.git/fs. +It provides a read-only view into the repository contents to allow +scripts to inspect the data. Surrounding scripts and binaries will +manipulate the repository contents directly. These changes will be +immediately mirrored in the file system. + +Scripts will generally mount git/fs as needed to do +their work, but if you want to browse the repository +manually, run it yourself. You'll get `$repo/.git/fs` mounted, +with the following contents: + + $repo/.git/fs/object: The objects in the repo. + $repo/.git/fs/branch: The branches in the repo. + $repo/.git/fs/ctl: A file showing the status of the repo. + Currently, it only shows the current branch. + $repo/.git/fs/HEAD An alias for the currently checked out + commit directory. + +Visible Differences +------------------- + +The most obvious difference is that Git's index is a bit boneheaded, so I'm +ignoring it. The index doesn't affect the wire protocol, so this +isn't an interoperability issue, unless you share the same physical +repository on both Plan 9 and Unix. If you do, expect them to disagree +about the files that have been modified in the working copy. + +In fact, the entire concept of the staging area has been dropped, as +it's both confusing and clunky. There are now only three states that +files can be in: 'untracked', 'dirty', and 'committed'. Tracking is +done with empty files under .git/index9/{removed,tracked}/path/to/file. + +It's implemented in Plan 9 flavor C, and provides tools for writing +repository contents, and a file system for read-only access, which +will mirror the current state of the repository. + +Installation +------------ + +Install with `mk install`. + +Examples +-------- + +Some usage examples: + + git/clone git://git.eigenstate.org/ori/mc.git + git/log + cd subdir/name + git/add foo.c + diff bar.c $repo/.git/fs/HEAD/ + git/commit foo.c + git/push + +Commits are presented as directories with the following +contents: + + author: A file containing the author name + hash: A file containing the commit hash + parent: A file containing the commit parents, one per line. + msg: A file containing the log message for that commit + tree: A directory containing a view of the repository. + +So, for example: + + % ls $repo/.git/fs/branch/heads/master + $repo/.git/fs/branch/heads/master/author + $repo/.git/fs/branch/heads/master/hash + $repo/.git/fs/branch/heads/master/msg + $repo/.git/fs/branch/heads/master/parent + $repo/.git/fs/branch/heads/master/tree + % cat $repo/.git/fs/branch/heads/master/hash + 7d539a7c08aba3f31b3913e0efef11c43ea9 + + # This is the same commit, with the same contents. + % ls $repo/.git/fs/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef + $repo/.git/fs/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef/author + $repo/.git/fs/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef/hash + $repo/.git/fs/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef/msg + $repo/.git/fs/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef/parent + $repo/.git/fs/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef/tree + + # what git/diff will hopefully do more concisely soon, filtering + # out the non-git files. + ape/diff -ur $repo/.git/fs/branch/heads/master/tree . + Only in .: .git + Only in .: debug + diff -ur $repo/.git/fs/branch/heads/master/tree/fold.myr ./fold.myr + --- $repo/.git/fs/branch/heads/master/tree/fold.myr Wed Dec 31 16:00:00 1969 + +++ ./fold.myr Mon Apr 1 21:39:06 2019 + @@ -6,6 +6,8 @@ + const foldexpr : (e : expr# -> std.option(constval)) + ;; + + +/* Look, diffing files just works, and I don't need any fancy glue! */ + + + const foldexpr = {e + match e + | &(`Eident &[.sc=`Sclassenum, .name=name, .ty=`Tyenum &(`Body enum)]): + Only in .: refs + + +The following utilities and binaries are provided: + + fs: The git filesystem. + fetch: The protocol bits for getting data from a git server. + send: The protocol bits for sending data to a git server. + save: The gnarly bits for storing the files for a commit. + conf: A program to extract information from a config file. + clone: Clones a repository. + commit: Commits a snapshot of the selected files. + log: Prints the contents of a commmit log. + add: Tells the repository to add a file to the next commit. + walk: `du`, but for git status. + ... and more + +Supported protocols: git:// and git+ssh://. If someone +implements others, I'll gladly accept patches. + +9legacy Notes +------------- + +To use git9 on 9legacy, these patches are necessary. + + - rc-line-split: implement delim{...} syntax for rc + - walk: port walk command to 9legacy. + - aux/getflags: 9front updated it with support for named args + +Additionally, git9 defaults to editing with hold(1). This command +is not present on 9legacy. Import it, or set $editor to your choice +of editor. diff --git a/sys/src/cmd/git/add b/sys/src/cmd/git/add new file mode 100755 index 000000000..b980d7dc8 --- /dev/null +++ b/sys/src/cmd/git/add @@ -0,0 +1,38 @@ +#!/bin/rc -e +rfork ne +. /sys/lib/git/common.rc + +gitup + +flagfmt='r:remove'; args='file ...' +eval `''{aux/getflags $*} || exec aux/usage + +add='tracked' +del='removed' +if(~ $remove 1){ + add='removed' + del='tracked' +} +if(~ $#* 0) + exec aux/usage + +paths=`$nl{cleanname -d $gitrel $* | drop $gitroot} +if(~ $add tracked) + files=`$nl{walk -f ./$paths} +if not + files=`$nl{cd .git/index9/tracked/ && walk -f ./$paths} + +for(f in $files){ + if(! ~ `$nl{cleanname $f} .git/*){ + addpath=.git/index9/$add/$f + delpath=.git/index9/$del/$f + mkdir -p `$nl{basename -d $addpath} + mkdir -p `$nl{basename -d $delpath} + # We don't want a matching qid, so that + # git/walk doesn't think this came from + # a checkout. + echo -n > $addpath + rm -f $delpath + } +} +exit '' diff --git a/sys/src/cmd/git/branch b/sys/src/cmd/git/branch new file mode 100755 index 000000000..01c41176b --- /dev/null +++ b/sys/src/cmd/git/branch @@ -0,0 +1,122 @@ +#!/bin/rc -e +rfork en +. /sys/lib/git/common.rc + +gitup + +flagfmt='a:listall, b:baseref ref, d:delete, n:newbr, s:stay, m:merge' +args='[branch]' +eval `''{aux/getflags $*} || exec aux/usage + +modified=() +deleted=() + +if(~ $#* 0){ + if(~ $#listall 0) + awk '$1=="branch"{print $2}' < $gitfs/ctl + if not + cd .git/refs/ && walk -f heads remotes + exit +} +if(! ~ $#* 1) + exec aux/usage + +branch=$1 +if(~ $branch refs/heads/*) + new=$name +if not if(~ $branch heads/*) + new=refs/$branch +if not + new=refs/heads/$branch + +orig=`{git/query HEAD} +if (~ $#baseref 1) + base=`{git/query $baseref} || exit 'bad base' +if not if(~ $#newbr 0) + base=`{git/query $new} +if not + base=`{git/query HEAD} + +if(~ $#newbr 0){ + if(! ~ $#baseref 0) + die update would clobber $branch with $baseref + baseref=`$nl{echo -n $new | sed s@refs/heads/@refs/remotes/origin/@} + if(! test -e .git/$new) + if(! base=`{git/query $baseref}) + die could not find branch $branch +} +modified=`$nl{git/query -c HEAD $base | grep '^[^-]' | subst '^..'} +deleted=`$nl{git/query -c HEAD $base | grep '^-' | subst '^..'} + +# if we're not merging, don't clobber existing changes. +if(~ $#merge 0){ + if(! ~ $#modified 0 || ! ~ $#deleted 0){ + git/walk -fRMA $modified $deleted || + die 'uncommitted changes would be clobbered' + } +} +if(~ $delete 1){ + rm -f .git/$new + echo 'deleted branch' $new + exit +} +commit=`{git/query $base} || die 'branch does not exist:' $base +if(~ $new */*) + mkdir -p .git/`{basename -d $new} +if(! ~ $#stay 0){ + echo $commit > .git/$new + exit +} +basedir=`{git/query -p $base} +dirtypaths=() +if(! ~ $#modified 0 || ! ~ $#deleted 0) + dirtypaths=`$nl{git/walk -cfRMA $modified $deleted} +if(~ $#dirtypaths 0) + cleanpaths=($modified $deleted) +if not { + cleanpaths=() + for(p in $modified $deleted) + if(! ~ $p $dirtypaths) + cleanpaths=($cleanpaths $p) +} + +echo $commit > .git/$new +for(m in $cleanpaths){ + d=`$nl{basename -d $m} + mkdir -p $d + mkdir -p .git/index9/tracked/$d + # Modifications can turn a file into + # a directory, or vice versa, so we + # need to delete and copy the files + # over. + a=dir + b=dir + if(test -f $m) + a=file + if(test -f $basedir/tree/$m) + b=file + if(! ~ $a $b){ + rm -rf $m + rm -rf .git/index9/tracked/$m + } + if(~ $b file){ + cp -x -- $basedir/tree/$m $m + walk -eq $m > .git/index9/tracked/$m + touch $m + } +} + +for(ours in $dirtypaths){ + common=$gitfs/object/$orig/tree/$ours + theirs=$gitfs/object/$base/tree/$ours + merge1 $ours $ours $common $theirs +} + +if(! ~ $#deleted 0){ + rm -f $deleted + rm -f .git/index9/tracked/$deleted +} + +echo ref: $new > .git/HEAD +echo $new: `{git/query $new} +exit '' diff --git a/sys/src/cmd/git/clone b/sys/src/cmd/git/clone new file mode 100755 index 000000000..93e7f0b97 --- /dev/null +++ b/sys/src/cmd/git/clone @@ -0,0 +1,114 @@ +#!/bin/rc +rfork en +. /sys/lib/git/common.rc + +flagfmt='d:debug, b:branch branch'; args='remote [local]' +eval `''{aux/getflags $*} || exec aux/usage +if(~ $debug 1) + debug=(-d) + +remote=`{echo $1 | sed 's@/*$@@'} +local=$2 + +if(~ $#remote 0) + exec aux/usage +if(~ $#local 0) + local=`{basename $remote .git} +if(~ $#branch 1) + branchflag=(-b $branch) + +if(test -e $local) + die 'repository already exists:' $local + +fn clone{ + flag +e + mkdir -p $local/.git + mkdir -p $local/.git/fs + mkdir -p $local/.git/objects/pack/ + mkdir -p $local/.git/refs/heads/ + + cd $local + + >>.git/config { + echo '[remote "origin"]' + echo ' url='$remote + } + {git/get $debug $branchflag $remote >[2=3] | awk ' + BEGIN{ + headref="" + if(ENVIRON["branch"] != "") + headref="refs/remotes/origin/"ENVIRON["branch"] + headhash="" + } + /^symref / && headref == "" { + if($2 == "HEAD"){ + gsub("^refs/heads", "refs/remotes/origin", $3) + gsub("^refs/tags", "refs/remotes/origin/tags", $3) + } + } + /^remote /{ + if($2=="HEAD"){ + headhash=$3 + }else if(match($2, "^refs/(heads|tags)/")){ + gsub("^refs/heads", "refs/remotes/origin", $2) + if($2 == headref || (headref == "" && $3 == headhash)) + headref=$2 + outfile = ".git/" $2 + outdir = outfile + gsub("/?[^/]*/?$", "", outdir) + system("mkdir -p "outdir) + print $3 > outfile + close(outfile) + } + } + END{ + if(headref != ""){ + remote = headref; + refdir = headref; + gsub("/?[^/]*/?$", "", refdir) + gsub("^refs/remotes/origin", "refs/heads", headref) + system("mkdir -p .git/"refdir); + system("cp .git/" remote " .git/" headref) + print "ref: " headref > ".git/HEAD" + }else if(headhash != ""){ + print "warning: detached head "headhash > "/fd/2" + print headhash > ".git/HEAD" + } + } + '} |[3] tr '\x0d' '\x0a' || die 'could not clone repository' + + tree=.git/fs/HEAD/tree + lbranch=`{git/branch} + rbranch=`{echo $lbranch | subst 'heads' 'remotes/origin'} + echo checking out repository... + if(test -f .git/refs/$rbranch){ + cp .git/refs/$rbranch .git/refs/$lbranch + git/fs + @ {builtin cd $tree && tar cif /fd/1 .} | @ {tar xf /fd/0} \ + || die 'checkout failed:' $status + for(f in `$nl{walk -f $tree | drop $tree}){ + idx=.git/index9/tracked/$f + mkdir -p `$nl{basename -d $idx} + walk -eq ./$f > $idx + } + } + if not{ + echo no default branch >[1=2] + echo check out your code with git/branch >[1=2] + } +} + +fn sigint { + echo cancelled clone $remote: cleaning $local >[1=2] + rm -rf $local + exit interrupted +} + +@{clone} +st=$status +if(! ~ $st ''){ + echo failed to clone $remote: cleaning $local >[1=2] + rm -rf $local + exit $st +} +exit '' diff --git a/sys/src/cmd/git/commit b/sys/src/cmd/git/commit new file mode 100644 index 000000000..5b61fe1eb --- /dev/null +++ b/sys/src/cmd/git/commit @@ -0,0 +1,136 @@ +#!/bin/rc -e +rfork ne +. /sys/lib/git/common.rc + +fn findbranch{ + branch=`{git/branch} + if(test -e $gitfs/branch/$branch/tree){ + refpath=.git/refs/$branch + initial=false + } + if not if(test -e $gitfs/object/$branch/tree){ + refpath=.git/HEAD + initial=false + } + if not if(! test -e $gitfs/HEAD/tree){ + refpath=.git/refs/$branch + initial=true + } + if not + die 'invalid branch:' $branch +} + +# Remove commentary lines. +# Remove leading and trailing empty lines. +# Combine consecutive empty lines between paragraphs. +# Remove trailing spaces from lines. +# Ensure there's trailing newline. +fn cleanmsg{ + awk ' + /^[ ]*#/ {next} + /^[ ]*$/ {empty = 1; next} + + wet && empty {printf "\n"} + {wet = 1; empty = 0} + {sub(/[ ]+$/, ""); print $0} + ' +} + +fn editmsg{ + if(! test -s $msgfile.tmp){ + >$msgfile.tmp { + echo '# Author:' $name '<'$email'>' + echo '#' + for(p in $parents) + echo '# parent:' $p + git/walk -fAMR $files | subst '^' '# ' + echo '#' + echo '# Commit message:' + } + edit=1 + } + if(! ~ $#edit 0){ + giteditor=`{git/conf core.editor} + if(~ $#editor 0) + editor=$giteditor + if(~ $#editor 0) + editor=hold + $editor $msgfile.tmp + } + cleanmsg < $msgfile.tmp > $msgfile + if(! test -s $msgfile) + die 'empty commit message' +} + +fn parents{ + if(! ~ $#revise 0) + parents=`{cat $gitfs/HEAD/parent} + if not if(test -f .git/index9/merge-parents) + parents=`{cat .git/index9/merge-parents | sort | uniq} + if not if(~ $initial true) + parents=() + if not + parents=`{git/query $branch} +} + +fn commit{ + msg=`''{cat $msgfile} + if(! ~ $#parents 0) + pflags='-p'^$parents + hash=`{git/save -n $"name -e $"email -m $"msg $pflags $files || die $status} + rm -f .git/index9/merge-parents +} + +fn update{ + mkdir -p `{basename -d $refpath} + # Paranoia: let's not mangle the repo. + if(~ $#hash 0) + die 'botched commit' + echo $branch: $hash + echo $hash > $refpath + for(f in $files){ + if(test -e .git/index9/removed/$f || ! test -e $f){ + rm -f .git/index9/removed/$f + rm -f .git/index9/tracked/$f + } + if not{ + mkdir -p `{basename -d $f} + walk -eq $f > .git/index9/tracked/$f + } + } +} + +fn sigexit{ + if(~ ! $#msgfile 0) + rm -f $msgfile $msgfile.tmp +} + +gitup + +flagfmt='m:msg message, r:revise, e:edit'; args='[file ...]' +eval `''{aux/getflags $*} || exec aux/usage + +msgfile=/tmp/git-msg.$pid +if(~ $#msg 1) + echo $msg >$msgfile.tmp +if not if(~ $#revise 1){ + msg=1 + echo revising commit `{cat $gitfs/HEAD/hash} + cat $gitfs/HEAD/msg >$msgfile.tmp +} + +files=() +if(! ~ $#* 0) + files=`$nl{git/walk -c `$nl{cleanname -d $gitrel $*}} +if(~ $status '' || ~ $#files 0 && ! test -f .git/index9/merge-parents && ~ $#revise 0) + die 'nothing to commit' $status +@{ + flag e + + whoami + findbranch + parents + editmsg + commit + update +} || die 'could not commit:' $status +exit '' diff --git a/sys/src/cmd/git/compat b/sys/src/cmd/git/compat new file mode 100755 index 000000000..25d14308f --- /dev/null +++ b/sys/src/cmd/git/compat @@ -0,0 +1,168 @@ +#!/bin/rc + +rfork e + +opts=() +args=() + +fn cmd_init{ + while(~ $#* 0){ + switch($1){ + case --bare + opts=(-b) + case -- + # go likes to use these + case -* + die unknown command init $* + case * + args=($args $1) + } + shift + } + ls >[1=2] + git/init $opts $args +} + +fn cmd_clone{ + branch=() + while( ! ~ $#* 0){ + switch($1){ + case -b + branch=$2 + shift + case -- + # go likes to use these + case -* + die unknown command clone $* + case * + args=($args $1) + } + shift + } + git/clone $opts $args + if(~ $#branch 1) + git/branch -n -b $1 origin/$1 +} + +fn cmd_pull{ + if(~ $1 -*) + die unknown options for pull $* + git/pull +} + +fn cmd_fetch{ + while(~ $#* 0){ + switch($1){ + case --all + opts=($opts -a) + case -f + opts=($opts -u $2) + shift + case -- + # go likes to use these + case -* + die unknown command clone $* + case * + args=($args $1) + } + shift + } + git/pull -f $opts +} + + +fn cmd_checkout{ + if(~ $1 -*) + die unknown command pull $* + if(~ $#* 0) + die git checkout branch + git/branch $b +} + +fn cmd_submodule { + if(test -f .gitmodules) + die 'submodules unsupported' +} + +fn cmd_rev-parse{ + while(~ $1 -*){ + switch($1){ + case --git-dir + echo $gitroot/.git + shift + case --abbrev-ref + echo `{dcmd git9/branch | sed s@^heads/@@g} + shift + case * + dprint option $opt + } + shift + } +} + +fn cmd_show-ref{ + if(~ $1 -*) + die unknown command pull $* + filter=cat + if(~ $#* 0) + filter=cat + if not + filter='-e(^|/)'^$*^'$' + for(b in `$nl{cd $gitroot/.git/refs/ && walk -f}) + echo `{cat $gitroot/.git/refs/$b} refs/$b +} + +fn cmd_remote{ + if({! ~ $#* 3 && ! ~ $#* 4} || ! ~ $1 add) + die unimplemented remote cmd $* + name=$2 + url=$3 + if(~ $3 '--') + url=$4 + >>$gitroot/.git/config{ + echo '[remote "'$name'"]' + echo ' url='$url + } +} + +fn cmd_ls-remote{ + if(~ $1 -q) + shift + remote=`$nl{git/conf 'remote "'$1'".url'} + if(~ $#remote 0) + remote=$1 + git/get -l $remote | awk '/^remote/{print $3"\t"$2}' +} + +fn cmd_version{ + echo git version 2.2.0 +} + + +fn usage{ + echo 'git ' >[1=2] + exit usage +} + +fn die { + >[1=2] echo git $_cmdname: $* + exit $_cmdname: $* +} + +_cmdname=$1 +if(~ $0 *compat){ + ramfs -m /n/gitcompat + touch /n/gitcompat/git + bind $0 /n/gitcompat/git + path=( /n/gitcompat $path ) + exec rc +} + +if(! test -f '/env/fn#cmd_'$1) + die git $1: commmand not implemented +if(! ~ $1 init && ! ~ $1 clone) + gitroot=`{git/conf -r} || die repo + +echo $* >/tmp/gitlog +cmd_$1 $*(2-) +exit '' diff --git a/sys/src/cmd/git/conf.c b/sys/src/cmd/git/conf.c new file mode 100644 index 000000000..63cf475d5 --- /dev/null +++ b/sys/src/cmd/git/conf.c @@ -0,0 +1,98 @@ +#include +#include +#include + +#include "git.h" + +int findroot; +int showall; +int nfile; +char *file[32]; + +static int +showconf(char *cfg, char *sect, char *key) +{ + char *ln, *p; + Biobuf *f; + int foundsect, nsect, nkey, found; + + if((f = Bopen(cfg, OREAD)) == nil) + return 0; + + found = 0; + nsect = sect ? strlen(sect) : 0; + nkey = strlen(key); + foundsect = (sect == nil); + while((ln = Brdstr(f, '\n', 1)) != nil){ + p = strip(ln); + if(*p == '[' && sect){ + foundsect = strncmp(sect, ln, nsect) == 0; + }else if(foundsect && strncmp(p, key, nkey) == 0){ + p = strip(p + nkey); + if(*p != '=') + continue; + p = strip(p + 1); + print("%s\n", p); + found = 1; + if(!showall){ + free(ln); + goto done; + } + } + free(ln); + } +done: + return found; +} + + +void +usage(void) +{ + fprint(2, "usage: %s [-f file] [-r] keys..\n", argv0); + fprint(2, "\t-f: use file 'file' (default: .git/config)\n"); + fprint(2, "\t r: print repository root\n"); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char repo[512], *p, *s; + int i, j; + + ARGBEGIN{ + case 'f': file[nfile++]=EARGF(usage()); break; + case 'r': findroot++; break; + case 'a': showall++; break; + default: usage(); break; + }ARGEND; + + if(findroot){ + if(findrepo(repo, sizeof(repo)) == -1) + sysfatal("%r"); + print("%s\n", repo); + exits(nil); + } + if(nfile == 0){ + file[nfile++] = ".git/config"; + if((p = getenv("home")) != nil) + file[nfile++] = smprint("%s/lib/git/config", p); + file[nfile++] = "/sys/lib/git/config"; + } + + for(i = 0; i < argc; i++){ + if((p = strchr(argv[i], '.')) == nil){ + s = nil; + p = argv[i]; + }else{ + *p = 0; + p++; + s = smprint("[%s]", argv[i]); + } + for(j = 0; j < nfile; j++) + if(showconf(file[j], s, p)) + break; + } + exits(nil); +} diff --git a/sys/src/cmd/git/delta.c b/sys/src/cmd/git/delta.c new file mode 100644 index 000000000..32af58b64 --- /dev/null +++ b/sys/src/cmd/git/delta.c @@ -0,0 +1,219 @@ +#include +#include + +#include "git.h" + +enum { + Minchunk = 128, + Maxchunk = 8192, + Splitmask = (1<<8)-1, + +}; + +static u32int geartab[] = { + 0x67ed26b7, 0x32da500c, 0x53d0fee0, 0xce387dc7, 0xcd406d90, 0x2e83a4d4, 0x9fc9a38d, 0xb67259dc, + 0xca6b1722, 0x6d2ea08c, 0x235cea2e, 0x3149bb5f, 0x1beda787, 0x2a6b77d5, 0x2f22d9ac, 0x91fc0544, + 0xe413acfa, 0x5a30ff7a, 0xad6fdde0, 0x444fd0f5, 0x7ad87864, 0x58c5ff05, 0x8d2ec336, 0x2371f853, + 0x550f8572, 0x6aa448dd, 0x7c9ddbcf, 0x95221e14, 0x2a82ec33, 0xcbec5a78, 0xc6795a0d, 0x243995b7, + 0x1c909a2f, 0x4fded51c, 0x635d334b, 0x0e2b9999, 0x2702968d, 0x856de1d5, 0x3325d60e, 0xeb6a7502, + 0xec2a9844, 0x0905835a, 0xa1820375, 0xa4be5cab, 0x96a6c058, 0x2c2ccd70, 0xba40fce3, 0xd794c46b, + 0x8fbae83e, 0xc3aa7899, 0x3d3ff8ed, 0xa0d42b5b, 0x571c0c97, 0xd2811516, 0xf7e7b96c, 0x4fd2fcbd, + 0xe2fdec94, 0x282cc436, 0x78e8e95c, 0x80a3b613, 0xcfbee20c, 0xd4a32d1c, 0x2a12ff13, 0x6af82936, + 0xe5630258, 0x8efa6a98, 0x294fb2d1, 0xdeb57086, 0x5f0fddb3, 0xeceda7ce, 0x4c87305f, 0x3a6d3307, + 0xe22d2942, 0x9d060217, 0x1e42ed02, 0xb6f63b52, 0x4367f39f, 0x055cf262, 0x03a461b2, 0x5ef9e382, + 0x386bc03a, 0x2a1e79c7, 0xf1a0058b, 0xd4d2dea9, 0x56baf37d, 0x5daff6cc, 0xf03a951d, 0xaef7de45, + 0xa8f4581e, 0x3960b555, 0xffbfff6d, 0xbe702a23, 0x8f5b6d6f, 0x061739fb, 0x98696f47, 0x3fd596d4, + 0x151eac6b, 0xa9fcc4f5, 0x69181a12, 0x3ac5a107, 0xb5198fe7, 0x96bcb1da, 0x1b5ddf8e, 0xc757d650, + 0x65865c3a, 0x8fc0a41a, 0x87435536, 0x99eda6f2, 0x41874794, 0x29cff4e8, 0xb70efd9a, 0x3103f6e7, + 0x84d2453b, 0x15a450bd, 0x74f49af1, 0x60f664b1, 0xa1c86935, 0xfdafbce1, 0xe36353e3, 0x5d9ba739, + 0xbc0559ba, 0x708b0054, 0xd41d808c, 0xb2f31723, 0x9027c41f, 0xf136d165, 0xb5374b12, 0x9420a6ac, + 0x273958b6, 0xe6c2fad0, 0xebdc1f21, 0xfb33af8b, 0xc71c25cd, 0xe9a2d8e5, 0xbeb38a50, 0xbceb7cc2, + 0x4e4e73f0, 0xcd6c251d, 0xde4c032c, 0x4b04ac30, 0x725b8b21, 0x4eb8c33b, 0x20d07b75, 0x0567aa63, + 0xb56b2bb7, 0xc1f5fd3a, 0xcafd35ca, 0x470dd4da, 0xfe4f94cd, 0xfb8de424, 0xe8dbcf40, 0xfe50a37a, + 0x62db5b5d, 0xf32f4ab6, 0x2c4a8a51, 0x18473dc0, 0xfe0cbb6e, 0xfe399efd, 0xdf34ecc9, 0x6ccd5055, + 0x46097073, 0x139135c2, 0x721c76f6, 0x1c6a94b4, 0x6eee014d, 0x8a508e02, 0x3da538f5, 0x280d394f, + 0x5248a0c4, 0x3ce94c6c, 0x9a71ad3a, 0x8493dd05, 0xe43f0ab6, 0x18e4ed42, 0x6c5c0e09, 0x42b06ec9, + 0x8d330343, 0xa45b6f59, 0x2a573c0c, 0xd7fd3de6, 0xeedeab68, 0x5c84dafc, 0xbbd1b1a8, 0xa3ce1ad1, + 0x85b70bed, 0xb6add07f, 0xa531309c, 0x8f8ab852, 0x564de332, 0xeac9ed0c, 0x73da402c, 0x3ec52761, + 0x43af2f4d, 0xd6ff45c8, 0x4c367462, 0xd553bd6a, 0x44724855, 0x3b2aa728, 0x56e5eb65, 0xeaf16173, + 0x33fa42ff, 0xd714bb5d, 0xfbd0a3b9, 0xaf517134, 0x9416c8cd, 0x534cf94f, 0x548947c2, 0x34193569, + 0x32f4389a, 0xfe7028bc, 0xed73b1ed, 0x9db95770, 0x468e3922, 0x0440c3cd, 0x60059a62, 0x33504562, + 0x2b229fbd, 0x5174dca5, 0xf7028752, 0xd63c6aa8, 0x31276f38, 0x0646721c, 0xb0191da8, 0xe00e6de0, + 0x9eac1a6e, 0x9f7628a5, 0xed6c06ea, 0x0bb8af15, 0xf119fb12, 0x38693c1c, 0x732bc0fe, 0x84953275, + 0xb82ec888, 0x33a4f1b3, 0x3099835e, 0x028a8782, 0x5fdd51d7, 0xc6c717b3, 0xb06caf71, 0x17c8c111, + 0x61bad754, 0x9fd03061, 0xe09df1af, 0x3bc9eb73, 0x85878413, 0x9889aaf2, 0x3f5a9e46, 0x42c9f01f, + 0x9984a4f4, 0xd5de43cc, 0xd294daed, 0xbecba2d2, 0xf1f6e72c, 0x5551128a, 0x83af87e2, 0x6f0342ba, +}; + +static u64int +hash(void *p, int n) +{ + uchar buf[SHA1dlen]; + sha1((uchar*)p, n, buf, nil); + return GETBE64(buf); +} + +static void +addblk(Dtab *dt, void *buf, int len, int off, u64int h) +{ + int i, sz, probe; + Dblock *db; + + probe = h % dt->sz; + while(dt->b[probe].buf != nil){ + if(len == dt->b[probe].len && memcmp(buf, dt->b[probe].buf, len) == 0) + return; + probe = (probe + 1) % dt->sz; + } + assert(dt->b[probe].buf == nil); + dt->b[probe].buf = buf; + dt->b[probe].len = len; + dt->b[probe].off = off; + dt->b[probe].hash = h; + dt->nb++; + if(dt->sz < 2*dt->nb){ + sz = dt->sz; + db = dt->b; + dt->sz *= 2; + dt->nb = 0; + dt->b = eamalloc(dt->sz, sizeof(Dblock)); + for(i = 0; i < sz; i++) + if(db[i].buf != nil) + addblk(dt, db[i].buf, db[i].len, db[i].off, db[i].hash); + free(db); + } +} + +static Dblock* +lookup(Dtab *dt, uchar *p, int n) +{ + int probe; + u64int h; + + h = hash(p, n); + for(probe = h % dt->sz; dt->b[probe].buf != nil; probe = (probe + 1) % dt->sz){ + if(dt->b[probe].hash != h) + continue; + if(n != dt->b[probe].len) + continue; + if(memcmp(p, dt->b[probe].buf, n) != 0) + continue; + return &dt->b[probe]; + } + return nil; +} + +static int +nextblk(uchar *s, uchar *e) +{ + u32int gh; + uchar *p; + + if((e - s) < Minchunk) + return e - s; + p = s + Minchunk; + if((e - s) > Maxchunk) + e = s + Maxchunk; + gh = 0; + while(p != e){ + gh = (gh<<1) + geartab[*p++]; + if((gh & Splitmask) == 0) + break; + } + return p - s; +} + +void +dtinit(Dtab *dt, Object *obj) +{ + uchar *s, *e; + u64int h; + vlong n, o; + + o = 0; + s = (uchar*)obj->data; + e = s + obj->size; + dt->o = ref(obj); + dt->nb = 0; + dt->sz = 128; + dt->b = eamalloc(dt->sz, sizeof(Dblock)); + dt->base = (uchar*)obj->data; + dt->nbase = obj->size; + while(s != e){ + n = nextblk(s, e); + h = hash(s, n); + addblk(dt, s, n, o, h); + s += n; + o += n; + } +} + +void +dtclear(Dtab *dt) +{ + unref(dt->o); + free(dt->b); +} + +static int +emitdelta(Delta **pd, int *nd, int cpy, int off, int len) +{ + Delta *d; + + *nd += 1; + *pd = earealloc(*pd, *nd, sizeof(Delta)); + d = &(*pd)[*nd - 1]; + d->cpy = cpy; + d->off = off; + d->len = len; + return len; +} + +static int +stretch(Dtab *dt, Dblock *b, uchar *s, uchar *e, int n) +{ + uchar *p, *q, *eb; + + if(b == nil) + return n; + p = s + n; + q = dt->base + b->off + n; + eb = dt->base + dt->nbase; + while(n < (1<<24)-1){ + if(p == e || q == eb) + break; + if(*p != *q) + break; + p++; + q++; + n++; + } + return n; +} + +Delta* +deltify(Object *obj, Dtab *dt, int *pnd) +{ + Delta *d; + Dblock *b; + uchar *s, *e; + vlong n, o; + + o = 0; + d = nil; + s = (uchar*)obj->data; + e = s + obj->size; + *pnd = 0; + while(s != e){ + n = nextblk(s, e); + b = lookup(dt, s, n); + n = stretch(dt, b, s, e, n); + if(b != nil) + emitdelta(&d, pnd, 1, b->off, n); + else + emitdelta(&d, pnd, 0, o, n); + s += n; + o += n; + } + return d; +} diff --git a/sys/src/cmd/git/diff b/sys/src/cmd/git/diff new file mode 100644 index 000000000..28e69d660 --- /dev/null +++ b/sys/src/cmd/git/diff @@ -0,0 +1,47 @@ +#!/bin/rc +rfork ne +. /sys/lib/git/common.rc + +gitup + +flagfmt='c:commit branch, s:summarize'; args='[file ...]' +eval `''{aux/getflags $*} || exec aux/usage + +if(~ $#commit 0) + commit=HEAD + +files=() +if(! ~ $#* 0) + files=`{cleanname -d $gitrel $*} + +branch=`{git/query -p $commit} +if(~ $summarize 1){ + git/walk -fMAR $files + exit +} + +fn lsdirty { + git/walk -c -fRMA $files + if(! ~ $commit HEAD) + git/query -c $commit HEAD | subst '^..' +} + +showed=() +mntgen /mnt/scratch +bind $branch/tree/ /mnt/scratch/a +bind . /mnt/scratch/b +for(f in `$nl{lsdirty | sort | uniq}){ + if(~ $#showed 0){ + echo diff `{git/query $commit} uncommitted + showed=1 + } + cd /mnt/scratch + a=a/$f + b=b/$f + if(! test -f a/$f) + a=/dev/null + if(! test -f b/$f) + b=/dev/null + diff -u $a $b +} +exit '' diff --git a/sys/src/cmd/git/export b/sys/src/cmd/git/export new file mode 100755 index 000000000..2d7c068d3 --- /dev/null +++ b/sys/src/cmd/git/export @@ -0,0 +1,88 @@ +#!/bin/rc +rfork ne +. /sys/lib/git/common.rc + +patchname=/tmp/git.patchname.$pid +patchfile=/tmp/git.patchfile.$pid +fn sigexit{ + rm -f $patchname $patchfile +} + +gitup + +flagfmt='o:patchdir patchdir'; args='[query]' +eval `''{aux/getflags $*} || exec aux/usage + +if(~ $#patchdir 1 && ! test -d $patchdir) + mkdir -p $patchdir + +q=$* +if(~ $#q 0) + q=HEAD +commits=`{git/query $q || die $status} +n=1 +m=$#commits + + +# sleazy hack: we want to run +# under rfork m for the web ui, +# so don't error if we can't mount +mntgen /mnt/scratch >[2]/dev/null || status='' +for(c in $commits){ + cp=`{git/query -p $c} + pp=`{git/query -p $c'~'} + fc=`$nl{git/query -c $c~ $c | sed 's/^..//'} + + @{ + rfork n + cd /mnt/scratch + if(test -d $pp/tree) + bind $pp/tree a + if(test -d $cp/tree) + bind $cp/tree b + + echo From: `{cat $cp/author} + echo Date: `{date -uf'WW, DD MMM YYYY hh:mm:ss Z' `{walk -em $cp/author}} + <$cp/msg awk ' + NR == 1 { + n = ENVIRON["n"] + m = ENVIRON["m"] + msg=$0 + if(m > 1) + patch = sprintf("[PATCH %d/%d]", n, m) + else + patch = "[PATCH]" + printf "Subject: %s %s\n\n", patch, msg + + gsub("^[ ]|[ ]$", "", msg) + gsub("[^a-zA-Z0-9_]+", "-", msg) + printf "%.4d-%s.patch", n, msg >ENVIRON["patchname"] + next + } + { + print + }' + echo '---' + echo diff `{basename $pp} `{basename $cp} + for(f in $fc){ + a=a/$f + if(! test -e $a) + a=/dev/null + b=b/$f + if(! test -e $b) + b=/dev/null + ape/diff -urN $a $b + } + } >$patchfile + if(~ $#patchdir 0){ + cat $patchfile + ! ~ $n $m && echo + } + if not{ + f=$patchdir/`{cat $patchname} + mv $patchfile $f + echo $f + } + n=`{echo $n + 1 | bc} +} +exit '' diff --git a/sys/src/cmd/git/extra/gitls b/sys/src/cmd/git/extra/gitls new file mode 100755 index 000000000..d9288c513 --- /dev/null +++ b/sys/src/cmd/git/extra/gitls @@ -0,0 +1,203 @@ +#!/bin/rc -e + +cd $1 +shift + +rfork ne +nl=' +' + +fn htcat { + sed ' + s/&/\&/g; + s//\>/g; + s/"/\"/g; + s/''/\'/g + ' $* +} + +fn resolveref { + if(~ $refname HEAD) + echo $refname + if not if(test -d $gitfs/branch/$refname/tree) + echo branch/$refname + if not if(test -d $gitfs/object/$refname/tree) + echo object/$refname + if not + status='bad ref' +} + +fn repons { + mntgen + mntgen /mnt/mnt + bind /bin /mnt/bin + bind /tmp /mnt/tmp + bind -c /env /mnt/env + bind $1 /mnt/$repo + bind /mnt / + cd /mnt/$repo + git/fs + rfork m +} + + +fn prelude { + echo ' + + + + + + + + git webls + + + ' +} + +switch($1){ +case 'tar' + repo=$2 + refname=$3 + @{ + repons $2 + if(! ref=`{resolveref $refname}){ + echo 'invalid ref '$refname'' + exit + } + bind $gitfs/$ref/tree /mnt/$repo + cd /mnt + tar cz $repo + } + +case 'list' + rfork m + prelude + echo '

Repos

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

Git: '$repo'

+

'$repo' @ '$hash' +

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

Code

+

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

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

About This Repo

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

Git: '$repo'

+

'$repo' @ '$hash' +

'
+	if(test -f $file){
+		htcat $file
+	}
+	if not if(test -d $file){
+		cd $file
+		for(f in `$nl{ls}){
+			url=`$nl{echo -n $f/f.html | urlencode}
+			fname=`$nl{echo -n $f | htcat}
+			echo ''$fname''
+		}
+	}
+	echo '	
+ + ' + } +} diff --git a/sys/src/cmd/git/extra/gitrules b/sys/src/cmd/git/extra/gitrules new file mode 100644 index 000000000..d01c8b13e --- /dev/null +++ b/sys/src/cmd/git/extra/gitrules @@ -0,0 +1,4 @@ +/repos.html /bin/gitls /usr/git list +/([^'/]+)/([^'/]+)/info.html /bin/gitls /usr/git info '\1' '\2' +/([^'/]+)/([^'/]+)/snap.tar.gz /bin/gitls /usr/git tar '\1' '\2' +/([^'/]+)/([^'/]+)/(([^']+)/)?f.html /bin/gitls /usr/git view '\1' '\2' '\4' diff --git a/sys/src/cmd/git/fs.c b/sys/src/cmd/git/fs.c new file mode 100644 index 000000000..7e6b4bf87 --- /dev/null +++ b/sys/src/cmd/git/fs.c @@ -0,0 +1,906 @@ +#include +#include +#include +#include +#include +#include <9p.h> + +#include "git.h" + +enum { + Qroot, + Qhead, + Qbranch, + Qcommit, + Qmsg, + Qparent, + Qtree, + Qcdata, + Qhash, + Qauthor, + Qcommitter, + Qobject, + Qctl, + Qmax, + Internal=1<<7, +}; + +typedef struct Gitaux Gitaux; +typedef struct Crumb Crumb; +typedef struct Cache Cache; +typedef struct Uqid Uqid; +struct Crumb { + char *name; + Object *obj; + Qid qid; + int mode; + vlong mtime; +}; + +struct Gitaux { + int ncrumb; + Crumb *crumb; + char *refpath; + int qdir; + + /* For listing object dir */ + Objlist *ols; + Object *olslast; +}; + +struct Uqid { + vlong uqid; + + vlong ppath; + vlong oid; + int t; + int idx; +}; + +struct Cache { + Uqid *cache; + int n; + int max; +}; + +char *qroot[] = { + "HEAD", + "branch", + "object", + "ctl", +}; + +#define Eperm "permission denied" +#define Eexist "does not exist" +#define E2long "path too long" +#define Enodir "not a directory" +#define Erepo "unable to read repo" +#define Eobject "invalid object" +#define Egreg "wat" +#define Ebadobj "invalid object" + +char gitdir[512]; +char *username; +char *groupname; +char *mntpt = ".git/fs"; +char **branches = nil; +Cache uqidcache[512]; +vlong nextqid = Qmax; + +static Object* walklink(Gitaux *, char *, int, int, int*); + +vlong +qpath(Crumb *p, int idx, vlong id, vlong t) +{ + int h, i; + vlong pp; + Cache *c; + Uqid *u; + + pp = p ? p->qid.path : 0; + h = (pp*333 + id*7 + t) & (nelem(uqidcache) - 1); + c = &uqidcache[h]; + u = c->cache; + for(i=0; i n ; i++){ + if(u->ppath == pp && u->oid == id && u->t == t && u->idx == idx) + return (u->uqid << 8) | t; + u++; + } + if(c->n == c->max){ + c->max += c->max/2 + 1; + c->cache = erealloc(c->cache, c->max*sizeof(Uqid)); + } + nextqid++; + c->cache[c->n] = (Uqid){nextqid, pp, id, t, idx}; + c->n++; + return (nextqid << 8) | t; +} + +static Crumb* +crumb(Gitaux *aux, int n) +{ + if(n < aux->ncrumb) + return &aux->crumb[aux->ncrumb - n - 1]; + return nil; +} + +static void +popcrumb(Gitaux *aux) +{ + Crumb *c; + + if(aux->ncrumb > 1){ + c = crumb(aux, 0); + free(c->name); + unref(c->obj); + aux->ncrumb--; + } +} + +static vlong +branchid(Gitaux *aux, char *path) +{ + int i; + + for(i = 0; branches[i]; i++) + if(strcmp(path, branches[i]) == 0) + goto found; + branches = realloc(branches, sizeof(char *)*(i + 2)); + branches[i] = estrdup(path); + branches[i + 1] = nil; + +found: + if(aux){ + if(aux->refpath) + free(aux->refpath); + aux->refpath = estrdup(branches[i]); + } + return i; +} + +static void +obj2dir(Dir *d, Crumb *c, Object *o, char *name) +{ + d->qid = c->qid; + d->atime = c->mtime; + d->mtime = c->mtime; + d->mode = c->mode; + d->name = estrdup9p(name); + d->uid = estrdup9p(username); + d->gid = estrdup9p(groupname); + d->muid = estrdup9p(username); + if(o->type == GBlob || o->type == GTag){ + d->qid.type = 0; + d->mode &= 0777; + d->length = o->size; + } + +} + +static int +rootgen(int i, Dir *d, void *p) +{ + Crumb *c; + + c = crumb(p, 0); + if (i >= nelem(qroot)) + return -1; + d->mode = 0555 | DMDIR; + d->name = estrdup9p(qroot[i]); + d->qid.vers = 0; + d->qid.type = strcmp(qroot[i], "ctl") == 0 ? 0 : QTDIR; + d->qid.path = qpath(nil, i, i, Qroot); + d->uid = estrdup9p(username); + d->gid = estrdup9p(groupname); + d->muid = estrdup9p(username); + d->mtime = c->mtime; + return 0; +} + +static int +branchgen(int i, Dir *d, void *p) +{ + Gitaux *aux; + Dir *refs; + Crumb *c; + int n; + + aux = p; + c = crumb(aux, 0); + refs = nil; + d->qid.vers = 0; + d->qid.type = QTDIR; + d->qid.path = qpath(c, i, branchid(aux, aux->refpath), Qbranch | Internal); + d->mode = 0555 | DMDIR; + d->uid = estrdup9p(username); + d->gid = estrdup9p(groupname); + d->muid = estrdup9p(username); + d->mtime = c->mtime; + d->atime = c->mtime; + if((n = slurpdir(aux->refpath, &refs)) < 0) + return -1; + if(i < n){ + d->name = estrdup9p(refs[i].name); + free(refs); + return 0; + }else{ + free(refs); + return -1; + } +} + +static int +gtreegen(int i, Dir *d, void *p) +{ + Object *o, *l, *e; + Gitaux *aux; + Crumb *c; + int m; + + aux = p; + c = crumb(aux, 0); + e = c->obj; + if(i >= e->tree->nent) + return -1; + m = e->tree->ent[i].mode; + if(e->tree->ent[i].ismod) + o = emptydir(); + else if((o = readobject(e->tree->ent[i].h)) == nil) + sysfatal("could not read object %H: %r", e->tree->ent[i].h); + if(e->tree->ent[i].islink) + if((l = walklink(aux, o->data, o->size, 0, &m)) != nil) + o = l; + d->qid.vers = 0; + d->qid.type = o->type == GTree ? QTDIR : 0; + d->qid.path = qpath(c, i, o->id, aux->qdir); + d->mode = m; + d->atime = c->mtime; + d->mtime = c->mtime; + d->uid = estrdup9p(username); + d->gid = estrdup9p(groupname); + d->muid = estrdup9p(username); + d->name = estrdup9p(e->tree->ent[i].name); + d->length = o->size; + return 0; +} + +static int +gcommitgen(int i, Dir *d, void *p) +{ + Object *o; + Crumb *c; + + c = crumb(p, 0); + o = c->obj; + d->uid = estrdup9p(username); + d->gid = estrdup9p(groupname); + d->muid = estrdup9p(username); + d->mode = 0444; + d->atime = o->commit->ctime; + d->mtime = o->commit->ctime; + d->qid.type = 0; + d->qid.vers = 0; + + switch(i){ + case 0: + d->mode = 0755 | DMDIR; + d->name = estrdup9p("tree"); + d->qid.type = QTDIR; + d->qid.path = qpath(c, i, o->id, Qtree); + break; + case 1: + d->name = estrdup9p("parent"); + d->qid.path = qpath(c, i, o->id, Qparent); + break; + case 2: + d->name = estrdup9p("msg"); + d->qid.path = qpath(c, i, o->id, Qmsg); + break; + case 3: + d->name = estrdup9p("hash"); + d->qid.path = qpath(c, i, o->id, Qhash); + break; + case 4: + d->name = estrdup9p("author"); + d->qid.path = qpath(c, i, o->id, Qauthor); + break; + default: + return -1; + } + return 0; +} + + +static int +objgen(int i, Dir *d, void *p) +{ + Gitaux *aux; + Object *o; + Crumb *c; + char name[64]; + Objlist *ols; + Hash h; + + aux = p; + c = crumb(aux, 0); + if(!aux->ols) + aux->ols = mkols(); + ols = aux->ols; + o = nil; + /* We tried to sent it, but it didn't fit */ + if(aux->olslast && ols->idx == i + 1){ + snprint(name, sizeof(name), "%H", aux->olslast->hash); + obj2dir(d, c, aux->olslast, name); + return 0; + } + while(ols->idx <= i){ + if(olsnext(ols, &h) == -1) + return -1; + if((o = readobject(h)) == nil){ + fprint(2, "corrupt object %H\n", h); + return -1; + } + } + if(o != nil){ + snprint(name, sizeof(name), "%H", o->hash); + obj2dir(d, c, o, name); + unref(aux->olslast); + aux->olslast = ref(o); + return 0; + } + return -1; +} + +static void +objread(Req *r, Gitaux *aux) +{ + Object *o; + + o = crumb(aux, 0)->obj; + switch(o->type){ + case GBlob: + readbuf(r, o->data, o->size); + break; + case GTag: + readbuf(r, o->data, o->size); + break; + case GTree: + dirread9p(r, gtreegen, aux); + break; + case GCommit: + dirread9p(r, gcommitgen, aux); + break; + default: + sysfatal("invalid object type %d", o->type); + } +} + +static void +readcommitparent(Req *r, Object *o) +{ + char *buf, *p, *e; + int i, n; + + /* 40 bytes per hash, 1 per nl, 1 for terminator */ + n = o->commit->nparent * (40 + 1) + 1; + buf = emalloc(n); + p = buf; + e = buf + n; + for (i = 0; i < o->commit->nparent; i++) + p = seprint(p, e, "%H\n", o->commit->parent[i]); + readbuf(r, buf, p - buf); + free(buf); +} + +static void +gitattach(Req *r) +{ + Gitaux *aux; + Dir *d; + + if((d = dirstat(".git")) == nil) + sysfatal("git/fs: %r"); + if(getwd(gitdir, sizeof(gitdir)) == nil) + sysfatal("getwd: %r"); + aux = emalloc(sizeof(Gitaux)); + aux->crumb = emalloc(sizeof(Crumb)); + aux->crumb[0].qid = (Qid){Qroot, 0, QTDIR}; + aux->crumb[0].obj = nil; + aux->crumb[0].mode = DMDIR | 0555; + aux->crumb[0].mtime = d->mtime; + aux->crumb[0].name = estrdup("/"); + aux->ncrumb = 1; + r->ofcall.qid = (Qid){Qroot, 0, QTDIR}; + r->fid->qid = r->ofcall.qid; + r->fid->aux = aux; + respond(r, nil); +} + +static Object* +walklink(Gitaux *aux, char *link, int nlink, int ndotdot, int *mode) +{ + char *p, *e, *path; + Object *o, *n; + int i; + + path = emalloc(nlink + 1); + memcpy(path, link, nlink); + cleanname(path); + + o = crumb(aux, ndotdot)->obj; + assert(o->type == GTree); + for(p = path; *p; p = e){ + n = nil; + e = p + strcspn(p, "/"); + if(*e == '/') + *e++ = '\0'; + /* + * cleanname guarantees these show up at the start of the name, + * which allows trimming them from the end of the trail of crumbs + * instead of needing to keep track of full parentage. + */ + if(strcmp(p, "..") == 0) + n = crumb(aux, ++ndotdot)->obj; + else if(o->type == GTree) + for(i = 0; i < o->tree->nent; i++) + if(strcmp(o->tree->ent[i].name, p) == 0){ + *mode = o->tree->ent[i].mode; + n = readobject(o->tree->ent[i].h); + break; + } + o = n; + if(o == nil) + break; + } + free(path); + return o; +} + +static char * +objwalk1(Qid *q, Object *o, Crumb *p, Crumb *c, char *name, vlong qdir, Gitaux *aux) +{ + Object *w, *l; + char *e; + int i, m; + + w = nil; + e = nil; + if(!o) + return Eexist; + if(o->type == GTree){ + q->type = 0; + for(i = 0; i < o->tree->nent; i++){ + if(strcmp(o->tree->ent[i].name, name) != 0) + continue; + m = o->tree->ent[i].mode; + w = readobject(o->tree->ent[i].h); + if(!w && o->tree->ent[i].ismod) + w = emptydir(); + if(w && o->tree->ent[i].islink) + if((l = walklink(aux, w->data, w->size, 1, &m)) != nil) + w = l; + if(!w) + return Ebadobj; + q->type = (w->type == GTree) ? QTDIR : 0; + q->path = qpath(c, i, w->id, qdir); + c->mode = m; + c->mode |= (w->type == GTree) ? DMDIR|0755 : 0644; + c->obj = w; + break; + } + if(!w) + e = Eexist; + }else if(o->type == GCommit){ + q->type = 0; + c->mtime = o->commit->mtime; + c->mode = 0644; + assert(qdir == Qcommit || qdir == Qobject || qdir == Qtree || qdir == Qhead || qdir == Qcommitter); + if(strcmp(name, "msg") == 0) + q->path = qpath(p, 0, o->id, Qmsg); + else if(strcmp(name, "parent") == 0) + q->path = qpath(p, 1, o->id, Qparent); + else if(strcmp(name, "hash") == 0) + q->path = qpath(p, 2, o->id, Qhash); + else if(strcmp(name, "author") == 0) + q->path = qpath(p, 3, o->id, Qauthor); + else if(strcmp(name, "committer") == 0) + q->path = qpath(p, 3, o->id, Qcommitter); + else if(strcmp(name, "tree") == 0){ + q->type = QTDIR; + q->path = qpath(p, 4, o->id, Qtree); + unref(c->obj); + c->mode = DMDIR | 0755; + c->obj = readobject(o->commit->tree); + if(c->obj == nil) + sysfatal("could not read object %H: %r", o->commit->tree); + } + else + e = Eexist; + }else if(o->type == GTag){ + e = "tag walk unimplemented"; + } + return e; +} + +static Object * +readref(char *pathstr) +{ + char buf[128], path[128], *p, *e; + Hash h; + int n, f; + + snprint(path, sizeof(path), "%s", pathstr); + while(1){ + if((f = open(path, OREAD)) == -1) + return nil; + if((n = readn(f, buf, sizeof(buf) - 1)) == -1) + return nil; + close(f); + buf[n] = 0; + if(strncmp(buf, "ref:", 4) != 0) + break; + + p = buf + 4; + while(isspace(*p)) + p++; + if((e = strchr(p, '\n')) != nil) + *e = 0; + snprint(path, sizeof(path), ".git/%s", p); + } + + if(hparse(&h, buf) == -1) + return nil; + + return readobject(h); +} + +static char* +gitwalk1(Fid *fid, char *name, Qid *q) +{ + char path[128]; + Gitaux *aux; + Crumb *c, *o; + char *e; + Dir *d; + Hash h; + + e = nil; + aux = fid->aux; + + q->vers = 0; + if(strcmp(name, "..") == 0){ + popcrumb(aux); + c = crumb(aux, 0); + *q = c->qid; + fid->qid = *q; + return nil; + } + + aux->crumb = realloc(aux->crumb, (aux->ncrumb + 1) * sizeof(Crumb)); + aux->ncrumb++; + c = crumb(aux, 0); + o = crumb(aux, 1); + memset(c, 0, sizeof(Crumb)); + c->mode = o->mode; + c->mtime = o->mtime; + c->obj = o->obj ? ref(o->obj) : nil; + + switch(QDIR(&fid->qid)){ + case Qroot: + if(strcmp(name, "HEAD") == 0){ + *q = (Qid){Qhead, 0, QTDIR}; + c->mode = DMDIR | 0555; + c->obj = readref(".git/HEAD"); + }else if(strcmp(name, "object") == 0){ + *q = (Qid){Qobject, 0, QTDIR}; + c->mode = DMDIR | 0555; + }else if(strcmp(name, "branch") == 0){ + *q = (Qid){Qbranch, 0, QTDIR}; + aux->refpath = estrdup(".git/refs/"); + c->mode = DMDIR | 0555; + }else if(strcmp(name, "ctl") == 0){ + *q = (Qid){Qctl, 0, 0}; + c->mode = 0644; + }else{ + e = Eexist; + } + break; + case Qbranch: + if(strcmp(aux->refpath, ".git/refs/heads") == 0 && strcmp(name, "HEAD") == 0) + snprint(path, sizeof(path), ".git/HEAD"); + else + snprint(path, sizeof(path), "%s/%s", aux->refpath, name); + q->type = QTDIR; + d = dirstat(path); + if(d && d->qid.type == QTDIR) + q->path = qpath(o, Qbranch, branchid(aux, path), Qbranch); + else if(d && (c->obj = readref(path)) != nil) + q->path = qpath(o, Qbranch, c->obj->id, Qcommit); + else + e = Eexist; + free(d); + break; + case Qobject: + if(c->obj){ + e = objwalk1(q, o->obj, o, c, name, Qobject, aux); + }else{ + if(hparse(&h, name) == -1) + return Eobject; + if((c->obj = readobject(h)) == nil) + return Eobject; + if(c->obj->type == GBlob || c->obj->type == GTag){ + c->mode = 0644; + q->type = 0; + }else{ + c->mode = DMDIR | 0755; + q->type = QTDIR; + } + q->path = qpath(o, Qobject, c->obj->id, Qobject); + q->vers = 0; + } + break; + case Qhead: + e = objwalk1(q, o->obj, o, c, name, Qhead, aux); + break; + case Qcommit: + e = objwalk1(q, o->obj, o, c, name, Qcommit, aux); + break; + case Qtree: + e = objwalk1(q, o->obj, o, c, name, Qtree, aux); + break; + case Qparent: + case Qmsg: + case Qcdata: + case Qhash: + case Qauthor: + case Qcommitter: + case Qctl: + return Enodir; + default: + return Egreg; + } + + c->name = estrdup(name); + c->qid = *q; + fid->qid = *q; + return e; +} + +static char* +gitclone(Fid *o, Fid *n) +{ + Gitaux *aux, *oaux; + int i; + + oaux = o->aux; + aux = emalloc(sizeof(Gitaux)); + aux->ncrumb = oaux->ncrumb; + aux->crumb = eamalloc(oaux->ncrumb, sizeof(Crumb)); + for(i = 0; i < aux->ncrumb; i++){ + aux->crumb[i] = oaux->crumb[i]; + aux->crumb[i].name = estrdup(oaux->crumb[i].name); + if(aux->crumb[i].obj) + aux->crumb[i].obj = ref(oaux->crumb[i].obj); + } + if(oaux->refpath) + aux->refpath = strdup(oaux->refpath); + aux->qdir = oaux->qdir; + n->aux = aux; + return nil; +} + +static void +gitdestroyfid(Fid *f) +{ + Gitaux *aux; + int i; + + if((aux = f->aux) == nil) + return; + for(i = 0; i < aux->ncrumb; i++){ + if(aux->crumb[i].obj) + unref(aux->crumb[i].obj); + free(aux->crumb[i].name); + } + olsfree(aux->ols); + free(aux->refpath); + free(aux->crumb); + free(aux); +} + +static char * +readctl(Req *r) +{ + char data[1024], ref[512], *s, *e; + int fd, n; + + if((fd = open(".git/HEAD", OREAD)) == -1) + return Erepo; + /* empty HEAD is invalid */ + if((n = readn(fd, ref, sizeof(ref) - 1)) <= 0) + return Erepo; + close(fd); + + s = ref; + ref[n] = 0; + if(strncmp(s, "ref:", 4) == 0) + s += 4; + while(*s == ' ' || *s == '\t') + s++; + if((e = strchr(s, '\n')) != nil) + *e = 0; + if(strstr(s, "refs/") == s) + s += strlen("refs/"); + + snprint(data, sizeof(data), "branch %s\nrepo %s\n", s, gitdir); + readstr(r, data); + return nil; +} + +static void +gitread(Req *r) +{ + char buf[256], *e; + Gitaux *aux; + Object *o; + Qid *q; + + aux = r->fid->aux; + q = &r->fid->qid; + o = crumb(aux, 0)->obj; + e = nil; + + switch(QDIR(q)){ + case Qroot: + dirread9p(r, rootgen, aux); + break; + case Qbranch: + if(o) + objread(r, aux); + else + dirread9p(r, branchgen, aux); + break; + case Qobject: + if(o) + objread(r, aux); + else + dirread9p(r, objgen, aux); + break; + case Qmsg: + readbuf(r, o->commit->msg, o->commit->nmsg); + break; + case Qparent: + readcommitparent(r, o); + break; + case Qhash: + snprint(buf, sizeof(buf), "%H\n", o->hash); + readstr(r, buf); + break; + case Qauthor: + snprint(buf, sizeof(buf), "%s\n", o->commit->author); + readstr(r, buf); + break; + case Qcommitter: + snprint(buf, sizeof(buf), "%s\n", o->commit->committer); + readstr(r, buf); + break; + case Qctl: + e = readctl(r); + break; + case Qhead: + /* Empty repositories have no HEAD */ + if(o == nil) + r->ofcall.count = 0; + else + objread(r, aux); + break; + case Qcommit: + case Qtree: + case Qcdata: + objread(r, aux); + break; + default: + e = Egreg; + } + respond(r, e); +} + +static void +gitopen(Req *r) +{ + Gitaux *aux; + Crumb *c; + + aux = r->fid->aux; + c = crumb(aux, 0); + switch(r->ifcall.mode&3){ + default: + respond(r, "botched mode"); + break; + case OWRITE: + respond(r, Eperm); + break; + case OREAD: + case ORDWR: + respond(r, nil); + break; + case OEXEC: + if((c->mode & 0111) == 0) + respond(r, Eperm); + else + respond(r, nil); + break; + } +} + +static void +gitstat(Req *r) +{ + Gitaux *aux; + Crumb *c; + + aux = r->fid->aux; + c = crumb(aux, 0); + r->d.uid = estrdup9p(username); + r->d.gid = estrdup9p(groupname); + r->d.muid = estrdup9p(username); + r->d.qid = r->fid->qid; + r->d.mtime = c->mtime; + r->d.atime = c->mtime; + r->d.mode = c->mode; + if(c->obj) + obj2dir(&r->d, c, c->obj, c->name); + else + r->d.name = estrdup9p(c->name); + respond(r, nil); +} + +Srv gitsrv = { + .attach=gitattach, + .walk1=gitwalk1, + .clone=gitclone, + .open=gitopen, + .read=gitread, + .stat=gitstat, + .destroyfid=gitdestroyfid, +}; + +void +usage(void) +{ + fprint(2, "usage: %s [-d]\n", argv0); + fprint(2, "\t-d: debug\n"); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + Dir *d; + + gitinit(); + ARGBEGIN{ + case 'd': + chatty9p++; + break; + case 'm': + mntpt = EARGF(usage()); + break; + default: + usage(); + break; + }ARGEND; + if(argc != 0) + usage(); + + if((d = dirstat(".git")) == nil) + sysfatal("dirstat .git: %r"); + username = strdup(d->uid); + groupname = strdup(d->gid); + free(d); + + branches = emalloc(sizeof(char*)); + branches[0] = nil; + postmountsrv(&gitsrv, nil, mntpt, MCREATE); + exits(nil); +} diff --git a/sys/src/cmd/git/get.c b/sys/src/cmd/git/get.c new file mode 100644 index 000000000..95dfe106e --- /dev/null +++ b/sys/src/cmd/git/get.c @@ -0,0 +1,372 @@ +#include +#include + +#include "git.h" + +char *fetchbranch; +char *upstream = "origin"; +int listonly; + +int +resolveremote(Hash *h, char *ref) +{ + char buf[128], *s; + int r, f; + + ref = strip(ref); + if((r = hparse(h, ref)) != -1) + return r; + /* Slightly special handling: translate remote refs to local ones. */ + if(strcmp(ref, "HEAD") == 0){ + snprint(buf, sizeof(buf), ".git/HEAD"); + }else if(strstr(ref, "refs/heads") == ref){ + ref += strlen("refs/heads"); + snprint(buf, sizeof(buf), ".git/refs/remotes/%s/%s", upstream, ref); + }else if(strstr(ref, "refs/tags") == ref){ + ref += strlen("refs/tags"); + snprint(buf, sizeof(buf), ".git/refs/tags/%s/%s", upstream, ref); + }else{ + return -1; + } + + r = -1; + s = strip(buf); + if((f = open(s, OREAD)) == -1) + return -1; + if(readn(f, buf, sizeof(buf)) >= 40) + r = hparse(h, buf); + close(f); + + if(r == -1 && strstr(buf, "ref:") == buf) + return resolveremote(h, buf + strlen("ref:")); + return r; +} + +int +rename(char *pack, char *idx, Hash h) +{ + char name[128]; + Dir st; + + nulldir(&st); + st.name = name; + snprint(name, sizeof(name), "%H.pack", h); + if(access(name, AEXIST) == 0) + fprint(2, "warning, pack %s already fetched\n", name); + else if(dirwstat(pack, &st) == -1) + return -1; + snprint(name, sizeof(name), "%H.idx", h); + if(access(name, AEXIST) == 0) + fprint(2, "warning, pack %s already indexed\n", name); + else if(dirwstat(idx, &st) == -1) + return -1; + return 0; +} + +int +checkhash(int fd, vlong sz, Hash *hcomp) +{ + DigestState *st; + Hash hexpect; + char buf[Pktmax]; + vlong n, r; + int nr; + + if(sz < 28){ + werrstr("undersize packfile"); + return -1; + } + + st = nil; + n = 0; + while(n != sz - 20){ + nr = sizeof(buf); + if(sz - n - 20 < sizeof(buf)) + nr = sz - n - 20; + r = readn(fd, buf, nr); + if(r != nr) + return -1; + st = sha1((uchar*)buf, nr, nil, st); + n += r; + } + sha1(nil, 0, hcomp->h, st); + if(readn(fd, hexpect.h, sizeof(hexpect.h)) != sizeof(hexpect.h)) + sysfatal("truncated packfile"); + if(!hasheq(hcomp, &hexpect)){ + werrstr("bad hash: %H != %H", *hcomp, hexpect); + return -1; + } + return 0; +} + +int +mkoutpath(char *path) +{ + char s[128]; + char *p; + int fd; + + snprint(s, sizeof(s), "%s", path); + for(p=strchr(s+1, '/'); p; p=strchr(p+1, '/')){ + *p = 0; + if(access(s, AEXIST) != 0){ + fd = create(s, OREAD, DMDIR | 0775); + if(fd == -1) + return -1; + close(fd); + } + *p = '/'; + } + return 0; +} + +int +branchmatch(char *br, char *pat) +{ + char name[128]; + + if(strstr(pat, "refs/heads") == pat) + snprint(name, sizeof(name), "%s", pat); + else if(strstr(pat, "heads")) + snprint(name, sizeof(name), "refs/%s", pat); + else + snprint(name, sizeof(name), "refs/heads/%s", pat); + return strcmp(br, name) == 0; +} + +char * +matchcap(char *s, char *cap, int full) +{ + if(strncmp(s, cap, strlen(cap)) == 0) + if(!full || strlen(s) == strlen(cap)) + return s + strlen(cap); + return nil; +} + +void +handlecaps(char *caps) +{ + char *p, *n, *c, *r; + + for(p = caps; p != nil; p = n){ + n = strchr(p, ' '); + if(n != nil) + *n++ = 0; + if((c = matchcap(p, "symref=", 0)) != nil){ + if((r = strchr(c, ':')) != nil){ + *r++ = '\0'; + print("symref %s %s\n", c, r); + } + } + } +} + +void +fail(char *pack, char *idx, char *msg, ...) +{ + char buf[ERRMAX]; + va_list ap; + + va_start(ap, msg); + snprint(buf, sizeof(buf), msg, ap); + va_end(ap); + + remove(pack); + remove(idx); + fprint(2, "%s", buf); + exits(buf); +} + +int +fetchpack(Conn *c) +{ + char buf[Pktmax], *sp[3], *ep; + char *packtmp, *idxtmp, **ref; + Hash h, *have, *want; + int nref, refsz, first; + int i, n, l, req, pfd; + vlong packsz; + Objset hadobj; + Object *o; + + nref = 0; + refsz = 16; + first = 1; + have = eamalloc(refsz, sizeof(have[0])); + want = eamalloc(refsz, sizeof(want[0])); + ref = eamalloc(refsz, sizeof(ref[0])); + while(1){ + n = readpkt(c, buf, sizeof(buf)); + if(n == -1) + return -1; + if(n == 0) + break; + if(strncmp(buf, "ERR ", 4) == 0) + sysfatal("%s", buf + 4); + + if(first && n > strlen(buf)) + handlecaps(buf + strlen(buf) + 1); + first = 0; + + getfields(buf, sp, nelem(sp), 1, " \t\n\r"); + if(strstr(sp[1], "^{}")) + continue; + if(fetchbranch && !branchmatch(sp[1], fetchbranch)) + continue; + if(refsz == nref + 1){ + refsz *= 2; + have = earealloc(have, refsz, sizeof(have[0])); + want = earealloc(want, refsz, sizeof(want[0])); + ref = earealloc(ref, refsz, sizeof(ref[0])); + } + if(hparse(&want[nref], sp[0]) == -1) + sysfatal("invalid hash %s", sp[0]); + if (resolveremote(&have[nref], sp[1]) == -1) + memset(&have[nref], 0, sizeof(have[nref])); + ref[nref] = estrdup(sp[1]); + nref++; + } + if(listonly){ + flushpkt(c); + goto showrefs; + } + + if(writephase(c) == -1) + sysfatal("write: %r"); + req = 0; + for(i = 0; i < nref; i++){ + if(hasheq(&have[i], &want[i])) + continue; + if((o = readobject(want[i])) != nil){ + unref(o); + continue; + } + n = snprint(buf, sizeof(buf), "want %H\n", want[i]); + if(writepkt(c, buf, n) == -1) + sysfatal("could not send want for %H", want[i]); + req = 1; + } + flushpkt(c); + osinit(&hadobj); + for(i = 0; i < nref; i++){ + if(hasheq(&have[i], &Zhash) || oshas(&hadobj, have[i])) + continue; + if((o = readobject(have[i])) == nil) + sysfatal("missing object we should have: %H", have[i]); + osadd(&hadobj, o); + unref(o); + n = snprint(buf, sizeof(buf), "have %H\n", have[i]); + if(writepkt(c, buf, n + 1) == -1) + sysfatal("could not send have for %H", have[i]); + } + osclear(&hadobj); + if(!req) + flushpkt(c); + + n = snprint(buf, sizeof(buf), "done\n"); + if(writepkt(c, buf, n) == -1) + sysfatal("write: %r"); + if(!req) + goto showrefs; + if(readphase(c) == -1) + sysfatal("read: %r"); + if((n = readpkt(c, buf, sizeof(buf))) == -1) + sysfatal("read: %r"); + buf[n] = 0; + + if((packtmp = smprint(".git/objects/pack/fetch.%d.pack", getpid())) == nil) + sysfatal("smprint: %r"); + if((idxtmp = smprint(".git/objects/pack/fetch.%d.idx", getpid())) == nil) + sysfatal("smprint: %r"); + if(mkoutpath(packtmp) == -1) + sysfatal("could not create %s: %r", packtmp); + if((pfd = create(packtmp, ORDWR, 0664)) == -1) + sysfatal("could not create %s: %r", packtmp); + + fprint(2, "fetching...\n"); + /* + * Work around torvalds git bug: we get duplicate have lines + * somtimes, even though the protocol is supposed to start the + * pack file immediately. + * + * Skip ahead until we read 'PACK' off the wire + */ + while(1){ + if(readn(c->rfd, buf, 4) != 4) + sysfatal("fetch packfile: short read"); + buf[4] = 0; + if(strncmp(buf, "PACK", 4) == 0) + break; + l = strtol(buf, &ep, 16); + if(l == 0 || ep != buf + 4) + sysfatal("fetch packfile: junk pktline"); + if(readn(c->rfd, buf, l) != l) + sysfatal("fetch packfile: short read"); + } + if(write(pfd, "PACK", 4) != 4) + sysfatal("write pack header: %r"); + packsz = 4; + while(1){ + n = read(c->rfd, buf, sizeof buf); + if(n == 0) + break; + if(n == -1 || write(pfd, buf, n) != n) + sysfatal("fetch packfile: %r"); + packsz += n; + } + + closeconn(c); + if(seek(pfd, 0, 0) == -1) + fail(packtmp, idxtmp, "packfile seek: %r"); + if(checkhash(pfd, packsz, &h) == -1) + fail(packtmp, idxtmp, "corrupt packfile: %r"); + close(pfd); + if(indexpack(packtmp, idxtmp, h) == -1) + fail(packtmp, idxtmp, "could not index fetched pack: %r"); + if(rename(packtmp, idxtmp, h) == -1) + fail(packtmp, idxtmp, "could not rename indexed pack: %r"); + +showrefs: + for(i = 0; i < nref; i++){ + print("remote %s %H local %H\n", ref[i], want[i], have[i]); + free(ref[i]); + } + free(ref); + free(want); + free(have); + return 0; +} + +void +usage(void) +{ + fprint(2, "usage: %s [-dl] [-b br] [-u upstream] remote\n", argv0); + fprint(2, "\t-b br: only fetch matching branch 'br'\n"); + fprint(2, "remote: fetch from this repository\n"); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + Conn c; + + ARGBEGIN{ + case 'b': fetchbranch=EARGF(usage()); break; + case 'u': upstream=EARGF(usage()); break; + case 'd': chattygit++; break; + case 'l': listonly++; break; + default: usage(); break; + }ARGEND; + + gitinit(); + if(argc != 1) + usage(); + + if(gitconnect(&c, argv[0], "upload") == -1) + sysfatal("could not dial %s: %r", argv[0]); + if(fetchpack(&c) == -1) + sysfatal("fetch failed: %r"); + closeconn(&c); + exits(nil); +} diff --git a/sys/src/cmd/git/git.h b/sys/src/cmd/git/git.h new file mode 100644 index 000000000..4e74333c5 --- /dev/null +++ b/sys/src/cmd/git/git.h @@ -0,0 +1,326 @@ +#include +#include +#include +#include +#include + +typedef struct Capset Capset; +typedef struct Conn Conn; +typedef struct Hash Hash; +typedef struct Delta Delta; +typedef struct Cinfo Cinfo; +typedef struct Tinfo Tinfo; +typedef struct Object Object; +typedef struct Objset Objset; +typedef struct Pack Pack; +typedef struct Buf Buf; +typedef struct Dirent Dirent; +typedef struct Idxent Idxent; +typedef struct Objlist Objlist; +typedef struct Dtab Dtab; +typedef struct Dblock Dblock; +typedef struct Objq Objq; +typedef struct Qelt Qelt; + +enum { + Pathmax = 512, + Npackcache = 32, + Hashsz = 20, + Pktmax = 65536, + KiB = 1024, + MiB = 1024*KiB, +}; + +enum { + GNone = 0, + GCommit = 1, + GTree = 2, + GBlob = 3, + GTag = 4, + GOdelta = 6, + GRdelta = 7, +}; + +enum { + Cloaded = 1 << 0, + Cidx = 1 << 1, + Ccache = 1 << 2, + Cexist = 1 << 3, + Cparsed = 1 << 5, + Cthin = 1 << 6, +}; + +enum { + ConnGit, + ConnGit9, + ConnSsh, + ConnHttp, +}; + +struct Objlist { + int idx; + + int fd; + int state; + int stage; + + Dir *top; + int ntop; + int topidx; + Dir *loose; + int nloose; + int looseidx; + Dir *pack; + int npack; + int packidx; + int nent; + int entidx; +}; + +struct Hash { + uchar h[20]; +}; + +struct Conn { + int type; + int rfd; + int wfd; + + /* only used by http */ + int cfd; + char *url; /* note, first GET uses a different url */ + char *dir; + char *direction; +}; + +struct Dirent { + char *name; + int mode; + Hash h; + char ismod; + char islink; +}; + +struct Object { + /* Git data */ + Hash hash; + int type; + + /* Cache */ + int id; + int flag; + int refs; + Object *next; + Object *prev; + + /* For indexing */ + vlong off; + vlong len; + u32int crc; + + /* Everything below here gets cleared */ + char *all; + char *data; + /* size excludes header */ + vlong size; + + /* Significant win on memory use */ + union { + Cinfo *commit; + Tinfo *tree; + }; +}; + +struct Tinfo { + /* Tree */ + Dirent *ent; + int nent; +}; + +struct Cinfo { + /* Commit */ + Hash *parent; + int nparent; + Hash tree; + char *author; + char *committer; + char *msg; + int nmsg; + vlong ctime; + vlong mtime; +}; + +struct Objset { + Object **obj; + int nobj; + int sz; +}; + +struct Qelt { + Object *o; + vlong ctime; + int color; +}; + +struct Objq { + Qelt *heap; + int nheap; + int heapsz; +}; + +struct Dtab { + Object *o; + uchar *base; + int nbase; + Dblock *b; + int nb; + int sz; +}; + +struct Dblock { + uchar *buf; + int len; + int off; + u64int hash; +}; + +struct Delta { + int cpy; + int off; + int len; +}; + + +#define GETBE16(b)\ + ((((b)[0] & 0xFFul) << 8) | \ + (((b)[1] & 0xFFul) << 0)) + +#define GETBE32(b)\ + ((((b)[0] & 0xFFul) << 24) | \ + (((b)[1] & 0xFFul) << 16) | \ + (((b)[2] & 0xFFul) << 8) | \ + (((b)[3] & 0xFFul) << 0)) +#define GETBE64(b)\ + ((((b)[0] & 0xFFull) << 56) | \ + (((b)[1] & 0xFFull) << 48) | \ + (((b)[2] & 0xFFull) << 40) | \ + (((b)[3] & 0xFFull) << 32) | \ + (((b)[4] & 0xFFull) << 24) | \ + (((b)[5] & 0xFFull) << 16) | \ + (((b)[6] & 0xFFull) << 8) | \ + (((b)[7] & 0xFFull) << 0)) + +#define PUTBE16(b, n)\ + do{ \ + (b)[0] = (n) >> 8; \ + (b)[1] = (n) >> 0; \ + } while(0) + +#define PUTBE32(b, n)\ + do{ \ + (b)[0] = (n) >> 24; \ + (b)[1] = (n) >> 16; \ + (b)[2] = (n) >> 8; \ + (b)[3] = (n) >> 0; \ + } while(0) + +#define PUTBE64(b, n)\ + do{ \ + (b)[0] = (n) >> 56; \ + (b)[1] = (n) >> 48; \ + (b)[2] = (n) >> 40; \ + (b)[3] = (n) >> 32; \ + (b)[4] = (n) >> 24; \ + (b)[5] = (n) >> 16; \ + (b)[6] = (n) >> 8; \ + (b)[7] = (n) >> 0; \ + } while(0) + +#define QDIR(qid) ((int)(qid)->path & (0xff)) +#define isblank(c) \ + (((c) != '\n') && isspace(c)) + +extern Reprog *authorpat; +extern Objset objcache; +extern vlong cachemax; +extern Hash Zhash; +extern int chattygit; +extern int interactive; + +#pragma varargck type "H" Hash +#pragma varargck type "T" int +#pragma varargck type "O" Object* +#pragma varargck type "Q" Qid +int Hfmt(Fmt*); +int Tfmt(Fmt*); +int Ofmt(Fmt*); +int Qfmt(Fmt*); + +void gitinit(void); + +/* object io */ +int resolverefs(Hash **, char *); +int resolveref(Hash *, char *); +int listrefs(Hash **, char ***); +Object *ancestor(Object *, Object *); +int findtwixt(Hash *, int, Hash *, int, Object ***, int *); +Object *readobject(Hash); +Object *clearedobject(Hash, int); +void parseobject(Object *); +int indexpack(char *, char *, Hash); +int writepack(int, Hash*, int, Hash*, int, Hash*); +int hasheq(Hash *, Hash *); +Object *ref(Object *); +void unref(Object *); +void cache(Object *); +Object *emptydir(void); + +/* object sets */ +void osinit(Objset *); +void osclear(Objset *); +void osadd(Objset *, Object *); +int oshas(Objset *, Hash); +Object *osfind(Objset *, Hash); + +/* object listing */ +Objlist *mkols(void); +int olsnext(Objlist *, Hash *); +void olsfree(Objlist *); + +/* util functions */ +#define dprint(lvl, ...) \ + if(chattygit >= lvl) _dprint(__VA_ARGS__) +void _dprint(char *, ...); +void *eamalloc(ulong, ulong); +void *emalloc(ulong); +void *earealloc(void *, ulong, ulong); +void *erealloc(void *, ulong); +char *estrdup(char *); +int slurpdir(char *, Dir **); +int hparse(Hash *, char *); +int hassuffix(char *, char *); +int swapsuffix(char *, int, char *, char *, char *); +char *strip(char *); +int findrepo(char *, int); +int showprogress(int, int); + +/* packing */ +void dtinit(Dtab *, Object*); +void dtclear(Dtab*); +Delta* deltify(Object*, Dtab*, int*); + +/* proto handling */ +int readpkt(Conn*, char*, int); +int writepkt(Conn*, char*, int); +int flushpkt(Conn*); +void initconn(Conn*, int, int); +int gitconnect(Conn *, char *, char *); +int readphase(Conn *); +int writephase(Conn *); +void closeconn(Conn *); + +/* queues */ +void qinit(Objq*); +void qclear(Objq*); +void qput(Objq*, Object*, int); +int qpop(Objq*, Qelt*); diff --git a/sys/src/cmd/git/import b/sys/src/cmd/git/import new file mode 100755 index 000000000..7ba7d0e48 --- /dev/null +++ b/sys/src/cmd/git/import @@ -0,0 +1,116 @@ +#!/bin/rc +rfork ne +. /sys/lib/git/common.rc + +diffpath=/tmp/gitimport.$pid.diff +fn sigexit { + rm -f $diffpath +} + + +fn apply @{ + git/fs + amail='' + aname='' + msg='' + whoami + parents='-p'^`{git/query HEAD} + branch=`{git/branch} + if(test -e $gitfs/branch/$branch/tree) + refpath=.git/refs/$branch + if not if(test -e $gitfs/object/$branch/tree) + refpath=.git/HEAD + if not + die 'invalid branch:' $branch + awk ' + BEGIN{ + state="headers" + } + state=="headers" && /^From:/ { + sub(/^From:[ \t]*/, "", $0); + aname=$0; + amail=$0; + sub(/[ \t]*<.*$/, "", aname); + sub(/^[^<]*[^>]*$/, "", amail); + } + state=="headers" && /^Date:/{ + sub(/^Date:[ \t]*/, "", $0) + date=$0 + } + state=="headers" && /^Subject:/{ + sub(/^Subject:[ \t]*(\[[^\]]*\][ \t]*)*/, "", $0); + gotmsg = 1 + print > "/env/msg" + } + state=="headers" && /^$/ { + state="body" + } + (state=="headers" || state=="body") && (/^diff / || /^---( |$)/){ + state="diff" + } + state=="body" && /^[ ]*$/ { + empty=1 + next + } + state=="body" { + if(empty) + printf "\n" > "/env/msg" + empty=0 + sub(/[ ]+$/, "") + print > "/env/msg" + } + state=="diff" { + print > ENVIRON["diffpath"] + } + END{ + if(state != "diff") + exit("malformed patch: " state); + if(aname == "" || amail == "" || date == "" || gotmsg == "") + exit("missing headers"); + printf "%s", aname > "/env/aname" + printf "%s", amail > "/env/amail" + printf "%s", date > "/env/date" + } + ' || die 'could not import:' $status + + # force re-reading env + rc -c ' + echo applying $msg | sed 1q + date=`{seconds $date} + if(! files=`$nl{ape/patch -Ep1 < $diffpath | grep ''^patching file'' | sed ''s/^patching file `(.*)''''/\1/''}) + die ''patch failed'' + for(f in $files){ + if(test -e $f) + git/add $f + if not + git/add -r $f + } + git/walk -fRMA $files + if(~ $#nocommit 0){ + if(hash=`{git/save -n $aname -e $amail -N $name -E $email -m $msg -d $date $parents $files}) + echo $hash > $refpath + } + status='''' + ' +} + +gitup + +flagfmt='n:nocommit'; args='file ...' +eval `''{aux/getflags $*} || exec aux/usage + +patches=(/fd/0) +if(! ~ $#* 0) + patches=$* +for(p in $patches){ + # upas serves the decoded header and body separately, + # so we cat them together when applying a upas message. + # + # this allows mime-encoded or line-wrapped patches. + if(test -d $p && test -f $p/header && test -f $p/body) + {{cat $p/header; echo; cat $p/body} | apply} || die $status + if not + apply < $p || die $status +} +exit '' diff --git a/sys/src/cmd/git/init b/sys/src/cmd/git/init new file mode 100755 index 000000000..50fa0270a --- /dev/null +++ b/sys/src/cmd/git/init @@ -0,0 +1,39 @@ +#!/bin/rc -e +rfork ne +. /sys/lib/git/common.rc + +flagfmt='u:upstream upstream,b:branch branch'; args='name' +eval `''{aux/getflags $*} || exec aux/usage + +dir=$1 +if(~ $#dir 0) + dir=. +if(~ $#branch 0) + branch=front +if(test -e $dir/.git) + die $dir/.git already exists +name=`{basename `{cleanname -d `{pwd} $dir}} +if(~ $#upstream 0){ + upstream=`{git/conf 'defaults "origin".baseurl'} + if(! ~ $#upstream 0) + upstream=$upstream/$name +} + +mkdir -p $dir/.git/refs/^(heads remotes) +mkdir -p $dir/.git/^(fs objects) +>$dir/.git/config { + echo '[core]' + echo ' repositoryformatversion = p9.0' + if(! ~ $#upstream 0){ + echo '[remote "origin"]' + echo ' url = '$upstream + } + echo '[branch "'$branch'"]' + echo ' remote = origin' +} + +>$dir/.git/HEAD { + echo ref: refs/heads/$branch +} + +exit '' diff --git a/sys/src/cmd/git/log.c b/sys/src/cmd/git/log.c new file mode 100644 index 000000000..719cd4f05 --- /dev/null +++ b/sys/src/cmd/git/log.c @@ -0,0 +1,282 @@ +#include +#include +#include "git.h" + +typedef struct Pfilt Pfilt; +struct Pfilt { + char *elt; + int show; + Pfilt *sub; + int nsub; +}; + +Biobuf *out; +char *queryexpr; +char *commitid; +int shortlog; + +Objset done; +Objq objq; +Pfilt *pathfilt; + +void +filteradd(Pfilt *pf, char *path) +{ + char *p, *e; + int i; + + if((e = strchr(path, '/')) != nil) + p = smprint("%.*s", (int)(e - path), path); + else + p = strdup(path); + + while(e != nil && *e == '/') + e++; + for(i = 0; i < pf->nsub; i++){ + if(strcmp(pf->sub[i].elt, p) == 0){ + pf->sub[i].show = pf->sub[i].show || (e == nil); + if(e != nil) + filteradd(&pf->sub[i], e); + free(p); + return; + } + } + pf->sub = earealloc(pf->sub, pf->nsub+1, sizeof(Pfilt)); + pf->sub[pf->nsub].elt = p; + pf->sub[pf->nsub].show = (e == nil); + pf->sub[pf->nsub].nsub = 0; + pf->sub[pf->nsub].sub = nil; + if(e != nil) + filteradd(&pf->sub[pf->nsub], e); + pf->nsub++; +} + +Hash +lookup(Pfilt *pf, Object *o) +{ + int i; + + for(i = 0; i < o->tree->nent; i++) + if(strcmp(o->tree->ent[i].name, pf->elt) == 0) + return o->tree->ent[i].h; + return Zhash; +} + +int +filtermatch1(Pfilt *pf, Object *t, Object *pt) +{ + Object *a, *b; + Hash ha, hb; + int i, r; + + if(pf->show) + return 1; + if(t->type != pt->type) + return 1; + if(t->type != GTree) + return 0; + + for(i = 0; i < pf->nsub; i++){ + ha = lookup(&pf->sub[i], t); + hb = lookup(&pf->sub[i], pt); + if(hasheq(&ha, &hb)) + continue; + if(hasheq(&ha, &Zhash) || hasheq(&hb, &Zhash)) + return 1; + if((a = readobject(ha)) == nil) + sysfatal("read %H: %r", ha); + if((b = readobject(hb)) == nil) + sysfatal("read %H: %r", hb); + r = filtermatch1(&pf->sub[i], a, b); + unref(a); + unref(b); + if(r) + return 1; + } + return 0; +} + +int +filtermatch(Object *o) +{ + Object *t, *p, *pt; + int i, r; + + if(pathfilt == nil) + return 1; + if((t = readobject(o->commit->tree)) == nil) + sysfatal("read %H: %r", o->commit->tree); + for(i = 0; i < o->commit->nparent; i++){ + if((p = readobject(o->commit->parent[i])) == nil) + sysfatal("read %H: %r", o->commit->parent[i]); + if((pt = readobject(p->commit->tree)) == nil) + sysfatal("read %H: %r", o->commit->tree); + r = filtermatch1(pathfilt, t, pt); + unref(p); + unref(pt); + if(r) + return 1; + } + return o->commit->nparent == 0; +} + + +static char* +nextline(char *p, char *e) +{ + for(; p != e; p++) + if(*p == '\n') + break; + return p; +} + +static void +show(Object *o) +{ + Tm tm; + char *p, *q, *e; + + assert(o->type == GCommit); + if(!filtermatch(o)) + return; + + if(shortlog){ + p = o->commit->msg; + e = p + o->commit->nmsg; + q = nextline(p, e); + Bprint(out, "%H ", o->hash); + Bwrite(out, p, q - p); + Bputc(out, '\n'); + }else{ + Bprint(out, "Hash:\t%H\n", o->hash); + Bprint(out, "Author:\t%s\n", o->commit->author); + if(o->commit->committer != nil + && strcmp(o->commit->author, o->commit->committer) != 0) + Bprint(out, "Committer:\t%s\n", o->commit->committer); + Bprint(out, "Date:\t%s\n", ctime(o->commit->mtime)); + Bprint(out, "\n"); + p = o->commit->msg; + e = p + o->commit->nmsg; + for(; p != e; p = q){ + q = nextline(p, e); + Bputc(out, '\t'); + Bwrite(out, p, q - p); + Bputc(out, '\n'); + if(q != e) + q++; + } + Bprint(out, "\n"); + } + Bflush(out); +} + +static void +showquery(char *q) +{ + Object *o; + Hash *h; + int n, i; + + if((n = resolverefs(&h, q)) == -1) + sysfatal("resolve: %r"); + for(i = 0; i < n; i++){ + if((o = readobject(h[i])) == nil) + sysfatal("read %H: %r", h[i]); + show(o); + unref(o); + } + exits(nil); +} + +static void +showcommits(char *c) +{ + Object *o, *p; + Qelt e; + int i; + Hash h; + + if(c == nil) + c = "HEAD"; + if(resolveref(&h, c) == -1) + sysfatal("resolve %s: %r", c); + if((o = readobject(h)) == nil) + sysfatal("load %H: %r", h); + qinit(&objq); + osinit(&done); + qput(&objq, o, 0); + while(qpop(&objq, &e)){ + show(e.o); + for(i = 0; i < e.o->commit->nparent; i++){ + if(oshas(&done, e.o->commit->parent[i])) + continue; + if((p = readobject(e.o->commit->parent[i])) == nil) + sysfatal("load %H: %r", o->commit->parent[i]); + osadd(&done, p); + qput(&objq, p, 0); + } + unref(e.o); + } +} + +static void +usage(void) +{ + fprint(2, "usage: %s [-s] [-e expr | -c commit] files..\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char path[1024], repo[1024], *p, *r; + int i, nrepo; + + ARGBEGIN{ + case 'e': + queryexpr = EARGF(usage()); + break; + case 'c': + commitid = EARGF(usage()); + break; + case 's': + shortlog++; + break; + default: + usage(); + break; + }ARGEND; + + if(findrepo(repo, sizeof(repo)) == -1) + sysfatal("find root: %r"); + nrepo = strlen(repo); + if(argc != 0){ + if(getwd(path, sizeof(path)) == nil) + sysfatal("getwd: %r"); + if(strncmp(path, repo, nrepo) != 0) + sysfatal("path shifted??"); + p = path + nrepo; + pathfilt = emalloc(sizeof(Pfilt)); + for(i = 0; i < argc; i++){ + if(*argv[i] == '/'){ + if(strncmp(argv[i], repo, nrepo) != 0) + continue; + r = smprint("./%s", argv[i]+nrepo); + }else + r = smprint("./%s/%s", p, argv[i]); + cleanname(r); + filteradd(pathfilt, r); + free(r); + } + } + if(chdir(repo) == -1) + sysfatal("chdir: %r"); + + gitinit(); + out = Bfdopen(1, OWRITE); + if(queryexpr != nil) + showquery(queryexpr); + else + showcommits(commitid); + exits(nil); +} diff --git a/sys/src/cmd/git/merge b/sys/src/cmd/git/merge new file mode 100755 index 000000000..051e22483 --- /dev/null +++ b/sys/src/cmd/git/merge @@ -0,0 +1,46 @@ +#!/bin/rc -e +rfork ne +. /sys/lib/git/common.rc + +fn merge{ + ourbr=$gitfs/object/$1/tree + basebr=$gitfs/object/$2/tree + theirbr=$gitfs/object/$3/tree + + all=`$nl{{git/query -c $1 $2; git/query -c $2 $3} | sed 's/^..//' | sort | uniq} + for(f in $all){ + ours=$ourbr/$f + base=$basebr/$f + theirs=$theirbr/$f + merge1 ./$f $ours $base $theirs + } +} + +gitup + +flagfmt=''; args='theirs' +eval `''{aux/getflags $*} || exec aux/usage + +if(! ~ $#* 1) + exec aux/usage + +theirs=`{git/query $1} +ours=`{git/query HEAD} +base=`{git/query $theirs ^ ' ' ^ $ours ^ '@'} + +if(~ $base $theirs) + die 'nothing to merge, doofus' +if(! git/walk -q) + die 'dirty work tree, refusing to merge' +if(~ $base $ours){ + >[1=2] echo 'fast forwarding...' + echo $theirs > .git/refs/`{git/branch} + git/revert . + exit '' +} +echo $ours >> .git/index9/merge-parents +echo $theirs >> .git/index9/merge-parents + +merge $ours $base $theirs +>[1=2] echo 'merge complete: remember to commit' +exit '' diff --git a/sys/src/cmd/git/mkfile b/sys/src/cmd/git/mkfile new file mode 100644 index 000000000..e8df434ff --- /dev/null +++ b/sys/src/cmd/git/mkfile @@ -0,0 +1,57 @@ + +#include + +#include "git.h" + +void +osinit(Objset *s) +{ + s->sz = 16; + s->nobj = 0; + s->obj = eamalloc(s->sz, sizeof(Hash)); +} + +void +osclear(Objset *s) +{ + free(s->obj); +} + +void +osadd(Objset *s, Object *o) +{ + u32int probe; + Object **obj; + int i, sz; + + probe = GETBE32(o->hash.h) % s->sz; + while(s->obj[probe]){ + if(hasheq(&s->obj[probe]->hash, &o->hash)){ + s->obj[probe] = o; + return; + } + probe = (probe + 1) % s->sz; + } + assert(s->obj[probe] == nil); + s->obj[probe] = o; + s->nobj++; + if(s->sz < 2*s->nobj){ + sz = s->sz; + obj = s->obj; + + s->sz *= 2; + s->nobj = 0; + s->obj = eamalloc(s->sz, sizeof(Hash)); + for(i = 0; i < sz; i++) + if(obj[i]) + osadd(s, obj[i]); + free(obj); + } +} + +Object* +osfind(Objset *s, Hash h) +{ + u32int probe; + + for(probe = GETBE32(h.h) % s->sz; s->obj[probe]; probe = (probe + 1) % s->sz) + if(hasheq(&s->obj[probe]->hash, &h)) + return s->obj[probe]; + return 0; +} + +int +oshas(Objset *s, Hash h) +{ + return osfind(s, h) != nil; +} diff --git a/sys/src/cmd/git/ols.c b/sys/src/cmd/git/ols.c new file mode 100644 index 000000000..3b77d3741 --- /dev/null +++ b/sys/src/cmd/git/ols.c @@ -0,0 +1,170 @@ +#include +#include +#include +#include "git.h" + +enum { + Sinit, + Siter, +}; + +static int +crackidx(char *path, int *np) +{ + int fd; + char buf[4]; + + if((fd = open(path, OREAD)) == -1) + return -1; + if(seek(fd, 8 + 255*4, 0) == -1) + return -1; + if(readn(fd, buf, sizeof(buf)) != sizeof(buf)) + return -1; + *np = GETBE32(buf); + return fd; +} + +int +isloosedir(char *s) +{ + return strlen(s) == 2 && isxdigit(s[0]) && isxdigit(s[1]); +} + +int +endswith(char *n, char *s) +{ + int nn, ns; + + nn = strlen(n); + ns = strlen(s); + return nn > ns && strcmp(n + nn - ns, s) == 0; +} + +int +olsreadpacked(Objlist *ols, Hash *h) +{ + char *p; + int i, j; + + i = ols->packidx; + j = ols->entidx; + + if(ols->state == Siter) + goto step; + for(i = 0; i < ols->npack; i++){ + if(!endswith(ols->pack[i].name, ".idx")) + continue; + if((p = smprint(".git/objects/pack/%s", ols->pack[i].name)) == nil) + sysfatal("smprint: %r"); + ols->fd = crackidx(p, &ols->nent); + free(p); + if(ols->fd == -1) + continue; + j = 0; + while(j < ols->nent){ + if(readn(ols->fd, h->h, sizeof(h->h)) != sizeof(h->h)) + continue; + ols->state = Siter; + ols->packidx = i; + ols->entidx = j; + return 0; +step: + j++; + } + close(ols->fd); + } + ols->state = Sinit; + return -1; +} + + +int +olsreadloose(Objlist *ols, Hash *h) +{ + char buf[64], *p; + int i, j, n; + + i = ols->topidx; + j = ols->looseidx; + if(ols->state == Siter) + goto step; + for(i = 0; i < ols->ntop; i++){ + if(!isloosedir(ols->top[i].name)) + continue; + if((p = smprint(".git/objects/%s", ols->top[i].name)) == nil) + sysfatal("smprint: %r"); + ols->fd = open(p, OREAD); + free(p); + if(ols->fd == -1) + continue; + while((ols->nloose = dirread(ols->fd, &ols->loose)) > 0){ + j = 0; + while(j < ols->nloose){ + n = snprint(buf, sizeof(buf), "%s%s", ols->top[i].name, ols->loose[j].name); + if(n >= sizeof(buf)) + goto step; + if(hparse(h, buf) == -1) + goto step; + ols->state = Siter; + ols->topidx = i; + ols->looseidx = j; + return 0; +step: + j++; + } + free(ols->loose); + ols->loose = nil; + } + close(ols->fd); + ols->fd = -1; + } + ols->state = Sinit; + return -1; +} + +Objlist* +mkols(void) +{ + Objlist *ols; + + ols = emalloc(sizeof(Objlist)); + if((ols->ntop = slurpdir(".git/objects", &ols->top)) == -1) + sysfatal("read top level: %r"); + if((ols->npack = slurpdir(".git/objects/pack", &ols->pack)) == -1) + ols->pack = nil; + ols->fd = -1; + return ols; +} + +void +olsfree(Objlist *ols) +{ + if(ols == nil) + return; + if(ols->fd != -1) + close(ols->fd); + free(ols->top); + free(ols->loose); + free(ols->pack); + free(ols); +} + +int +olsnext(Objlist *ols, Hash *h) +{ + if(ols->stage == 0){ + if(olsreadloose(ols, h) != -1){ + ols->idx++; + return 0; + } + ols->stage++; + } + if(ols->stage == 1){ + if(olsreadpacked(ols, h) != -1){ + ols->idx++; + return 0; + } + ols->stage++; + } + return -1; +} diff --git a/sys/src/cmd/git/pack.c b/sys/src/cmd/git/pack.c new file mode 100644 index 000000000..34431730f --- /dev/null +++ b/sys/src/cmd/git/pack.c @@ -0,0 +1,1757 @@ +#include +#include +#include + +#include "git.h" + +typedef struct Buf Buf; +typedef struct Metavec Metavec; +typedef struct Meta Meta; +typedef struct Compout Compout; +typedef struct Packf Packf; + +#define max(x, y) ((x) > (y) ? (x) : (y)) + +struct Metavec { + Meta **meta; + int nmeta; + int metasz; +}; + +struct Meta { + Object *obj; + char *path; + vlong mtime; + + /* The best delta we picked */ + Meta *head; + Meta *prev; + Delta *delta; + int ndelta; + int nchain; + + /* Only used for delta window */ + Dtab dtab; + + /* Only used for writing offset deltas */ + vlong off; +}; + +struct Compout { + Biobuf *bfd; + DigestState *st; +}; + +struct Buf { + int len; + int sz; + int off; + char *data; +}; + +struct Packf { + char path[128]; + char *idx; + vlong nidx; + + int refs; + Biobuf *pack; + vlong opentm; +}; + +static int readpacked(Biobuf *, Object *, int); +static Object *readidxobject(Biobuf *, Hash, int); + +Objset objcache; +Object *lruhead; +Object *lrutail; +vlong ncache; +vlong cachemax = 512*MiB; +Packf *packf; +int npackf; +int openpacks; +int gitdirmode = -1; + +static void +clear(Object *o) +{ + if(!o) + return; + + assert(o->refs == 0); + assert((o->flag & Ccache) == 0); + assert(o->flag & Cloaded); + switch(o->type){ + case GCommit: + if(!o->commit) + break; + free(o->commit->parent); + free(o->commit->author); + free(o->commit->committer); + free(o->commit); + o->commit = nil; + break; + case GTree: + if(!o->tree) + break; + free(o->tree->ent); + free(o->tree); + o->tree = nil; + break; + default: + break; + } + + free(o->all); + o->all = nil; + o->data = nil; + o->flag &= ~(Cloaded|Cparsed); +} + +void +unref(Object *o) +{ + if(!o) + return; + o->refs--; + if(o->refs == 0) + clear(o); +} + +Object* +ref(Object *o) +{ + o->refs++; + return o; +} + +void +cache(Object *o) +{ + Object *p; + + if(o == lruhead) + return; + if(o == lrutail) + lrutail = lrutail->prev; + if(!(o->flag & Cexist)){ + osadd(&objcache, o); + o->id = objcache.nobj; + o->flag |= Cexist; + } + if(o->prev != nil) + o->prev->next = o->next; + if(o->next != nil) + o->next->prev = o->prev; + if(lrutail == o){ + lrutail = o->prev; + if(lrutail != nil) + lrutail->next = nil; + }else if(lrutail == nil) + lrutail = o; + if(lruhead) + lruhead->prev = o; + o->next = lruhead; + o->prev = nil; + lruhead = o; + + if(!(o->flag & Ccache)){ + o->flag |= Ccache; + ref(o); + ncache += o->size; + } + while(ncache > cachemax && lrutail != nil){ + p = lrutail; + lrutail = p->prev; + if(lrutail != nil) + lrutail->next = nil; + p->flag &= ~Ccache; + p->prev = nil; + p->next = nil; + ncache -= p->size; + unref(p); + } +} + +static int +loadpack(Packf *pf, char *name) +{ + char buf[128]; + int i, ifd; + Dir *d; + + memset(pf, 0, sizeof(Packf)); + snprint(buf, sizeof(buf), ".git/objects/pack/%s.idx", name); + snprint(pf->path, sizeof(pf->path), ".git/objects/pack/%s.pack", name); + /* + * if we already have the pack open, just + * steal the loaded info + */ + for(i = 0; i < npackf; i++){ + if(strcmp(pf->path, packf[i].path) == 0){ + pf->pack = packf[i].pack; + pf->idx = packf[i].idx; + pf->nidx = packf[i].nidx; + packf[i].idx = nil; + packf[i].pack = nil; + return 0; + } + } + if((ifd = open(buf, OREAD)) == -1) + return -1; + if((d = dirfstat(ifd)) == nil){ + close(ifd); + return -1; + } + pf->nidx = d->length; + pf->idx = emalloc(pf->nidx); + if(readn(ifd, pf->idx, pf->nidx) != pf->nidx){ + close(ifd); + free(pf->idx); + free(d); + return -1; + } + close(ifd); + free(d); + return 0; +} + +static void +refreshpacks(void) +{ + Packf *pf, *new; + int i, n, l, nnew; + Dir *d; + + if((n = slurpdir(".git/objects/pack", &d)) == -1) + return; + nnew = 0; + new = eamalloc(n, sizeof(Packf)); + for(i = 0; i < n; i++){ + l = strlen(d[i].name); + if(l > 4 && strcmp(d[i].name + l - 4, ".idx") != 0) + continue; + d[i].name[l - 4] = 0; + if(loadpack(&new[nnew], d[i].name) != -1) + nnew++; + } + for(i = 0; i < npackf; i++){ + pf = &packf[i]; + free(pf->idx); + if(pf->pack != nil) + Bterm(pf->pack); + } + free(packf); + packf = new; + npackf = nnew; + free(d); +} + +static Biobuf* +openpack(Packf *pf) +{ + vlong t; + int i, best; + + if(pf->pack != nil){ + pf->refs++; + return pf->pack; + } + /* + * If we've got more packs open + * than we want cached, try to + * free up the oldest ones. + * + * If we can't find a slot, this + * isn't fatal; we can just use + * another fd. + */ + while(openpacks >= Npackcache){ + t = (1ull<<62)-1; + best = -1; + for(i = 0; i < npackf; i++){ + if(&packf[i] != pf + && packf[i].pack != nil + && packf[i].opentm < t + && packf[i].refs == 0){ + t = packf[i].opentm; + best = i; + } + } + if(best == -1){ + fprint(2, "no available pack slots\n"); + break; + } + Bterm(packf[best].pack); + packf[best].pack = nil; + openpacks--; + } + openpacks++; + pf->opentm = nsec(); + pf->refs++; + if((pf->pack = Bopen(pf->path, OREAD)) == nil) + return nil; + return pf->pack; +} + +static void +closepack(Packf *pf) +{ + pf->refs--; +} + +static u32int +crc32(u32int crc, char *b, int nb) +{ + static u32int crctab[256] = { + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, + 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, + 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, + 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, + 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, + 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, + 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, + 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, + 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, + 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, + 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, + 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, + 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, + 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, + 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, + 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, + 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, + 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, + 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, + 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, + 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, + 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, + 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, + 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, + 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, + 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, + 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, + 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, + 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, + 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, + 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d + }; + int i; + + crc ^= 0xFFFFFFFF; + for(i = 0; i < nb; i++) + crc = (crc >> 8) ^ crctab[(crc ^ b[i]) & 0xFF]; + return crc ^ 0xFFFFFFFF; +} + +int +bappend(void *p, void *src, int len) +{ + Buf *b = p; + char *n; + + while(b->len + len >= b->sz){ + b->sz = b->sz*2 + 64; + n = realloc(b->data, b->sz); + if(n == nil) + return -1; + b->data = n; + } + memmove(b->data + b->len, src, len); + b->len += len; + return len; +} + +int +breadc(void *p) +{ + return Bgetc(p); +} + +int +bdecompress(Buf *d, Biobuf *b, vlong *csz) +{ + vlong o; + + o = Boffset(b); + if(inflatezlib(d, bappend, b, breadc) == -1 || d->data == nil){ + free(d->data); + return -1; + } + if (csz) + *csz = Boffset(b) - o; + return d->len; +} + +int +decompress(void **p, Biobuf *b, vlong *csz) +{ + Buf d = {.len=0, .data=nil, .sz=0}; + + if(bdecompress(&d, b, csz) == -1){ + free(d.data); + return -1; + } + *p = d.data; + return d.len; +} + +static vlong +readvint(char *p, char **pp) +{ + int s, c; + vlong n; + + s = 0; + n = 0; + do { + c = *p++; + n |= (c & 0x7f) << s; + s += 7; + } while (c & 0x80 && s < 63); + *pp = p; + + return n; +} + +static int +applydelta(Object *dst, Object *base, char *d, int nd) +{ + char *r, *b, *ed, *er; + int n, nr, c; + vlong o, l; + + ed = d + nd; + b = base->data; + n = readvint(d, &d); + if(n != base->size){ + werrstr("mismatched source size"); + return -1; + } + + nr = readvint(d, &d); + r = emalloc(nr + 64); + n = snprint(r, 64, "%T %d", base->type, nr) + 1; + dst->all = r; + dst->type = base->type; + dst->data = r + n; + dst->size = nr; + er = dst->data + nr; + r = dst->data; + + while(d != ed){ + c = *d++; + /* copy from base */ + if(c & 0x80){ + o = 0; + l = 0; + /* Offset in base */ + if(d != ed && (c & 0x01)) o |= (*d++ << 0) & 0x000000ff; + if(d != ed && (c & 0x02)) o |= (*d++ << 8) & 0x0000ff00; + if(d != ed && (c & 0x04)) o |= (*d++ << 16) & 0x00ff0000; + if(d != ed && (c & 0x08)) o |= (*d++ << 24) & 0xff000000; + + /* Length to copy */ + if(d != ed && (c & 0x10)) l |= (*d++ << 0) & 0x0000ff; + if(d != ed && (c & 0x20)) l |= (*d++ << 8) & 0x00ff00; + if(d != ed && (c & 0x40)) l |= (*d++ << 16) & 0xff0000; + if(l == 0) l = 0x10000; + + if(o + l > base->size){ + werrstr("garbled delta: out of bounds copy"); + return -1; + } + memmove(r, b + o, l); + r += l; + /* inline data */ + }else{ + if(c > ed - d){ + werrstr("garbled delta: write past object"); + return -1; + } + memmove(r, d, c); + d += c; + r += c; + } + } + if(r != er){ + werrstr("truncated delta"); + return -1; + } + + return nr; +} + +static int +readrdelta(Biobuf *f, Object *o, int nd, int flag) +{ + Object *b; + Hash h; + char *d; + int n; + + d = nil; + if(Bread(f, h.h, sizeof(h.h)) != sizeof(h.h)) + goto error; + if(hasheq(&o->hash, &h)) + goto error; + if((n = decompress(&d, f, nil)) == -1) + goto error; + o->len = Boffset(f) - o->off; + if(d == nil || n != nd) + goto error; + if((b = readidxobject(f, h, flag|Cthin)) == nil) + goto error; + if(applydelta(o, b, d, n) == -1) + goto error; + free(d); + return 0; +error: + free(d); + return -1; +} + +static int +readodelta(Biobuf *f, Object *o, vlong nd, vlong p, int flag) +{ + Object b; + char *d; + vlong r; + int c, n; + + d = nil; + if((c = Bgetc(f)) == -1) + return -1; + r = c & 0x7f; + while(c & 0x80 && r < (1ULL<<56)){ + if((c = Bgetc(f)) == -1) + return -1; + r = ((r + 1)<<7) | (c & 0x7f); + } + + if(r > p || r >= (1ULL<<56)){ + werrstr("junk offset -%lld (from %lld)", r, p); + goto error; + } + if((n = decompress(&d, f, nil)) == -1) + goto error; + o->len = Boffset(f) - o->off; + if(d == nil || n != nd) + goto error; + if(Bseek(f, p - r, 0) == -1) + goto error; + memset(&b, 0, sizeof(Object)); + if(readpacked(f, &b, flag) == -1) + goto error; + if(applydelta(o, &b, d, nd) == -1) + goto error; + clear(&b); + free(d); + return 0; +error: + free(d); + return -1; +} + +static int +readpacked(Biobuf *f, Object *o, int flag) +{ + int c, s, n; + vlong l, p; + int t; + Buf b; + + p = Boffset(f); + c = Bgetc(f); + if(c == -1) + return -1; + l = c & 0xf; + s = 4; + t = (c >> 4) & 0x7; + if(!t){ + werrstr("unknown type for byte %x at %lld", c, p); + return -1; + } + while(c & 0x80){ + if((c = Bgetc(f)) == -1) + return -1; + l |= (c & 0x7f) << s; + s += 7; + } + if(l >= (1ULL << 32)){ + werrstr("object too big"); + return -1; + } + switch(t){ + default: + werrstr("invalid object at %lld", Boffset(f)); + return -1; + case GCommit: + case GTree: + case GTag: + case GBlob: + b.sz = 64 + l; + b.data = emalloc(b.sz); + n = snprint(b.data, 64, "%T %lld", t, l) + 1; + b.len = n; + if(bdecompress(&b, f, nil) == -1){ + free(b.data); + return -1; + } + o->len = Boffset(f) - o->off; + o->type = t; + o->all = b.data; + o->data = b.data + n; + o->size = b.len - n; + break; + case GOdelta: + if(readodelta(f, o, l, p, flag) == -1) + return -1; + break; + case GRdelta: + if(readrdelta(f, o, l, flag) == -1) + return -1; + break; + } + o->flag |= Cloaded|flag; + return 0; +} + +static int +readloose(Biobuf *f, Object *o, int flag) +{ + struct { char *tag; int type; } *p, types[] = { + {"blob", GBlob}, + {"tree", GTree}, + {"commit", GCommit}, + {"tag", GTag}, + {nil}, + }; + char *d, *s, *e; + vlong sz, n; + int l; + + n = decompress(&d, f, nil); + if(n == -1) + return -1; + + s = d; + o->type = GNone; + for(p = types; p->tag; p++){ + l = strlen(p->tag); + if(strncmp(s, p->tag, l) == 0){ + s += l; + o->type = p->type; + while(!isspace(*s)) + s++; + break; + } + } + if(o->type == GNone){ + free(o->data); + return -1; + } + sz = strtol(s, &e, 0); + if(e == s || *e++ != 0){ + werrstr("malformed object header"); + goto error; + } + if(sz != n - (e - d)){ + werrstr("mismatched sizes"); + goto error; + } + o->size = sz; + o->data = e; + o->all = d; + o->flag |= Cloaded|flag; + return 0; + +error: + free(d); + return -1; +} + +vlong +searchindex(char *idx, int nidx, Hash h) +{ + int lo, hi, hidx, i, r, nent; + vlong o, oo; + void *s; + + o = 8; + if(nidx < 8 + 256*4) + return -1; + /* + * Read the fanout table. The fanout table + * contains 256 entries, corresponsding to + * the first byte of the hash. Each entry + * is a 4 byte big endian integer, containing + * the total number of entries with a leading + * byte <= the table index, allowing us to + * rapidly do a binary search on them. + */ + if (h.h[0] == 0){ + lo = 0; + hi = GETBE32(idx + o); + } else { + o += h.h[0]*4 - 4; + lo = GETBE32(idx + o); + hi = GETBE32(idx + o + 4); + } + if(hi == lo) + goto notfound; + nent=GETBE32(idx + 8 + 255*4); + + /* + * Now that we know the range of hashes that the + * entry may exist in, search them + */ + r = -1; + hidx = -1; + o = 8 + 256*4; + while(lo < hi){ + hidx = (hi + lo)/2; + s = idx + o + hidx*sizeof(h.h); + r = memcmp(h.h, s, sizeof(h.h)); + if(r < 0) + hi = hidx; + else if(r > 0) + lo = hidx + 1; + else + break; + } + if(r != 0) + goto notfound; + + /* + * We found the entry. If it's 32 bits, then we + * can just return the oset, otherwise the 32 + * bit entry contains the oset to the 64 bit + * entry. + */ + oo = 8; /* Header */ + oo += 256*4; /* Fanout table */ + oo += Hashsz*nent; /* Hashes */ + oo += 4*nent; /* Checksums */ + oo += 4*hidx; /* Offset offset */ + if(oo < 0 || oo + 4 > nidx) + goto err; + i = GETBE32(idx + oo); + o = i & 0xffffffffULL; + /* + * Large offsets (i.e. 64-bit) are encoded as an index + * into the next table with the MSB bit set. + */ + if(o & (1ull << 31)){ + o &= 0x7fffffffULL; + oo = 8; /* Header */ + oo += 256*4; /* Fanout table */ + oo += Hashsz*nent; /* Hashes */ + oo += 4*nent; /* Checksums */ + oo += 4*nent; /* 32-bit Offsets */ + oo += 8*o; /* 64-bit Offset offset */ + if(oo < 0 || oo + 8 >= nidx) + goto err; + o = GETBE64(idx + oo); + } + return o; + +err: + werrstr("out of bounds read"); + return -1; +notfound: + werrstr("not present"); + return -1; +} + +/* + * Scans for non-empty word, copying it into buf. + * Strips off word, leading, and trailing space + * from input. + * + * Returns -1 on empty string or error, leaving + * input unmodified. + */ +static int +scanword(char **str, int *nstr, char *buf, int nbuf) +{ + char *p; + int n, r; + + r = -1; + p = *str; + n = *nstr; + while(n && isblank(*p)){ + n--; + p++; + } + + for(; n && *p && !isspace(*p); p++, n--){ + r = 0; + *buf++ = *p; + nbuf--; + if(nbuf == 0) + return -1; + } + while(n && isblank(*p)){ + n--; + p++; + } + *buf = 0; + *str = p; + *nstr = n; + return r; +} + +static void +nextline(char **str, int *nstr) +{ + char *s; + + if((s = strchr(*str, '\n')) != nil){ + *nstr -= s - *str + 1; + *str = s + 1; + } +} + +static int +parseauthor(char **str, int *nstr, char **name, vlong *time) +{ + char buf[128]; + Resub m[4]; + char *p; + int n, nm; + + if((p = strchr(*str, '\n')) == nil) + sysfatal("malformed author line"); + n = p - *str; + if(n >= sizeof(buf)) + sysfatal("overlong author line"); + memset(m, 0, sizeof(m)); + snprint(buf, n + 1, *str); + *str = p; + *nstr -= n; + + if(!regexec(authorpat, buf, m, nelem(m))) + sysfatal("invalid author line %s", buf); + nm = m[1].ep - m[1].sp; + *name = emalloc(nm + 1); + memcpy(*name, m[1].sp, nm); + buf[nm] = 0; + + nm = m[2].ep - m[2].sp; + memcpy(buf, m[2].sp, nm); + buf[nm] = 0; + *time = atoll(buf); + return 0; +} + +static void +parsecommit(Object *o) +{ + char *p, *t, buf[128]; + int np; + + p = o->data; + np = o->size; + o->commit = emalloc(sizeof(Cinfo)); + while(1){ + if(scanword(&p, &np, buf, sizeof(buf)) == -1) + break; + if(strcmp(buf, "tree") == 0){ + if(scanword(&p, &np, buf, sizeof(buf)) == -1) + sysfatal("invalid commit: tree missing"); + if(hparse(&o->commit->tree, buf) == -1) + sysfatal("invalid commit: garbled tree"); + }else if(strcmp(buf, "parent") == 0){ + if(scanword(&p, &np, buf, sizeof(buf)) == -1) + sysfatal("invalid commit: missing parent"); + o->commit->parent = realloc(o->commit->parent, ++o->commit->nparent * sizeof(Hash)); + if(!o->commit->parent) + sysfatal("unable to malloc: %r"); + if(hparse(&o->commit->parent[o->commit->nparent - 1], buf) == -1) + sysfatal("invalid commit: garbled parent"); + }else if(strcmp(buf, "author") == 0){ + parseauthor(&p, &np, &o->commit->author, &o->commit->mtime); + }else if(strcmp(buf, "committer") == 0){ + parseauthor(&p, &np, &o->commit->committer, &o->commit->ctime); + }else if(strcmp(buf, "gpgsig") == 0){ + /* just drop it */ + if((t = strstr(p, "-----END PGP SIGNATURE-----")) == nil) + sysfatal("malformed gpg signature"); + np -= t - p; + p = t; + } + nextline(&p, &np); + } + while (np && isspace(*p)) { + p++; + np--; + } + o->commit->msg = p; + o->commit->nmsg = np; +} + +static void +parsetree(Object *o) +{ + int m, a, entsz, nent; + Dirent *t, *ent; + char *p, *ep; + + p = o->data; + ep = p + o->size; + + nent = 0; + entsz = 16; + ent = eamalloc(entsz, sizeof(Dirent)); + o->tree = emalloc(sizeof(Tinfo)); + while(p != ep){ + if(nent == entsz){ + entsz *= 2; + ent = earealloc(ent, entsz, sizeof(Dirent)); + } + t = &ent[nent++]; + m = strtol(p, &p, 8); + if(*p != ' ') + sysfatal("malformed tree %H: *p=(%d) %c\n", o->hash, *p, *p); + p++; + /* + * only the stored permissions for the user + * are relevant; git fills group and world + * bits with whatever -- so to serve with + * useful permissions, replicate the mode + * of the git repo dir. + */ + a = (m & 0777)>>6; + t->mode = ((a<<6)|(a<<3)|a) & gitdirmode; + t->ismod = 0; + t->islink = 0; + if(m == 0160000){ + t->mode |= DMDIR; + t->ismod = 1; + }else if(m == 0120000){ + t->mode = 0; + t->islink = 1; + } + if(m & 0040000) + t->mode |= DMDIR; + t->name = p; + p = memchr(p, 0, ep - p); + if(*p++ != 0 || ep - p < sizeof(t->h.h)) + sysfatal("malformed tree %H, remaining %d (%s)", o->hash, (int)(ep - p), p); + memcpy(t->h.h, p, sizeof(t->h.h)); + p += sizeof(t->h.h); + } + o->tree->ent = ent; + o->tree->nent = nent; +} + +static void +parsetag(Object *) +{ +} + +void +parseobject(Object *o) +{ + if(o->flag & Cparsed) + return; + switch(o->type){ + case GTree: parsetree(o); break; + case GCommit: parsecommit(o); break; + case GTag: parsetag(o); break; + default: break; + } + o->flag |= Cparsed; +} + +static Object* +readidxobject(Biobuf *idx, Hash h, int flag) +{ + char path[Pathmax], hbuf[41]; + Object *obj, *new; + int i, r, retried; + Biobuf *f; + vlong o; + + if((obj = osfind(&objcache, h)) != nil){ + if(flag & Cidx){ + /* + * If we're indexing, we need to be careful + * to only return objects within this pack, + * so if the objects exist outside the pack, + * we don't index the wrong copy. + */ + if(!(obj->flag & Cidx)) + return nil; + if(obj->flag & Cloaded) + return obj; + o = Boffset(idx); + if(Bseek(idx, obj->off, 0) == -1) + return nil; + if(readpacked(idx, obj, flag) == -1) + return nil; + if(Bseek(idx, o, 0) == -1) + sysfatal("could not restore offset"); + cache(obj); + return obj; + } + if(obj->flag & Cloaded) + return obj; + } + if(flag & Cthin) + flag &= ~Cidx; + if(flag & Cidx) + return nil; + new = nil; + if(obj == nil){ + new = emalloc(sizeof(Object)); + new->id = objcache.nobj + 1; + new->hash = h; + obj = new; + } + + o = -1; + retried = 0; +retry: + for(i = 0; i < npackf; i++){ + o = searchindex(packf[i].idx, packf[i].nidx, h); + if(o != -1){ + if((f = openpack(&packf[i])) == nil) + goto error; + if((r = Bseek(f, o, 0)) != -1) + r = readpacked(f, obj, flag); + closepack(&packf[i]); + if(r == -1) + goto error; + parseobject(obj); + cache(obj); + return obj; + } + } + + snprint(hbuf, sizeof(hbuf), "%H", h); + snprint(path, sizeof(path), ".git/objects/%c%c/%s", hbuf[0], hbuf[1], hbuf + 2); + if((f = Bopen(path, OREAD)) != nil){ + if(readloose(f, obj, flag) == -1) + goto errorf; + Bterm(f); + parseobject(obj); + cache(obj); + return obj; + } + + if(o == -1){ + if(retried) + goto error; + retried = 1; + refreshpacks(); + goto retry; + } +errorf: + Bterm(f); +error: + free(new); + return nil; +} + +/* + * Loads and returns a cached object. + */ +Object* +readobject(Hash h) +{ + Object *o; + Dir *d; + + if(gitdirmode == -1){ + if((d = dirstat(".git")) == nil) + sysfatal("stat .git: %r"); + gitdirmode = d->mode & 0777; + free(d); + } + if((o = readidxobject(nil, h, 0)) == nil) + return nil; + parseobject(o); + ref(o); + return o; +} + +/* + * Creates and returns a cached, cleared object + * that will get loaded some other time. Useful + * for performance if need to mark that a blob + * exists, but we don't care about its contents. + * + * The refcount of the returned object is 0, so + * it doesn't need to be unrefed. + */ +Object* +clearedobject(Hash h, int type) +{ + Object *o; + + if((o = osfind(&objcache, h)) != nil) + return o; + + o = emalloc(sizeof(Object)); + o->hash = h; + o->type = type; + osadd(&objcache, o); + o->id = objcache.nobj; + o->flag |= Cexist; + return o; +} + +int +objcmp(void *pa, void *pb) +{ + Object *a, *b; + + a = *(Object**)pa; + b = *(Object**)pb; + return memcmp(a->hash.h, b->hash.h, sizeof(a->hash.h)); +} + +static int +hwrite(Biobuf *b, void *buf, int len, DigestState **st) +{ + *st = sha1(buf, len, nil, *st); + return Bwrite(b, buf, len); +} + +static u32int +objectcrc(Biobuf *f, Object *o) +{ + char buf[8096]; + int n, r; + + o->crc = 0; + Bseek(f, o->off, 0); + for(n = o->len; n > 0; n -= r){ + r = Bread(f, buf, n > sizeof(buf) ? sizeof(buf) : n); + if(r == -1) + return -1; + if(r == 0) + return 0; + o->crc = crc32(o->crc, buf, r); + } + return 0; +} + +int +indexpack(char *pack, char *idx, Hash ph) +{ + char hdr[4*3], buf[8]; + int nobj, npct, nvalid, nbig; + int i, n, pct; + Object *o, **obj; + DigestState *st; + char *valid; + Biobuf *f; + Hash h; + int c; + + if((f = Bopen(pack, OREAD)) == nil) + return -1; + if(Bread(f, hdr, sizeof(hdr)) != sizeof(hdr)){ + werrstr("short read on header"); + return -1; + } + if(memcmp(hdr, "PACK\0\0\0\2", 8) != 0){ + werrstr("invalid header"); + return -1; + } + + pct = 0; + npct = 0; + nvalid = 0; + nobj = GETBE32(hdr + 8); + obj = eamalloc(nobj, sizeof(Object*)); + valid = eamalloc(nobj, sizeof(char)); + if(interactive) + fprint(2, "indexing %d objects: 0%%", nobj); + while(nvalid != nobj){ + n = 0; + for(i = 0; i < nobj; i++){ + if(valid[i]){ + n++; + continue; + } + pct = showprogress((npct*100)/nobj, pct); + if(obj[i] == nil){ + o = emalloc(sizeof(Object)); + o->off = Boffset(f); + obj[i] = o; + } + o = obj[i]; + /* + * We can seek around when packing delta chains. + * Be extra careful while we don't know where all + * the objects start. + */ + Bseek(f, o->off, 0); + if(readpacked(f, o, Cidx) == -1) + continue; + sha1((uchar*)o->all, o->size + strlen(o->all) + 1, o->hash.h, nil); + valid[i] = 1; + cache(o); + npct++; + n++; + if(objectcrc(f, o) == -1) + return -1; + } + if(n == nvalid){ + sysfatal("fix point reached too early: %d/%d: %r", nvalid, nobj); + goto error; + } + nvalid = n; + } + if(interactive) + fprint(2, "\b\b\b\b100%%\n"); + Bterm(f); + + st = nil; + qsort(obj, nobj, sizeof(Object*), objcmp); + if((f = Bopen(idx, OWRITE)) == nil) + return -1; + if(hwrite(f, "\xfftOc\x00\x00\x00\x02", 8, &st) != 8) + goto error; + /* fanout table */ + c = 0; + for(i = 0; i < 256; i++){ + while(c < nobj && (obj[c]->hash.h[0] & 0xff) <= i) + c++; + PUTBE32(buf, c); + if(hwrite(f, buf, 4, &st) == -1) + goto error; + } + for(i = 0; i < nobj; i++){ + o = obj[i]; + if(hwrite(f, o->hash.h, sizeof(o->hash.h), &st) == -1) + goto error; + } + + for(i = 0; i < nobj; i++){ + PUTBE32(buf, obj[i]->crc); + if(hwrite(f, buf, 4, &st) == -1) + goto error; + } + + nbig = 0; + for(i = 0; i < nobj; i++){ + if(obj[i]->off < (1ull<<31)) + PUTBE32(buf, obj[i]->off); + else{ + PUTBE32(buf, (1ull << 31) | nbig); + nbig++; + } + if(hwrite(f, buf, 4, &st) == -1) + goto error; + } + for(i = 0; i < nobj; i++){ + if(obj[i]->off >= (1ull<<31)){ + PUTBE64(buf, obj[i]->off); + if(hwrite(f, buf, 8, &st) == -1) + goto error; + } + } + if(hwrite(f, ph.h, sizeof(ph.h), &st) == -1) + goto error; + sha1(nil, 0, h.h, st); + Bwrite(f, h.h, sizeof(h.h)); + + free(obj); + free(valid); + Bterm(f); + return 0; + +error: + free(obj); + free(valid); + Bterm(f); + return -1; +} + +static int +deltaordercmp(void *pa, void *pb) +{ + Meta *a, *b; + int cmp; + + a = *(Meta**)pa; + b = *(Meta**)pb; + if(a->obj->type != b->obj->type) + return a->obj->type - b->obj->type; + cmp = strcmp(a->path, b->path); + if(cmp != 0) + return cmp; + if(a->mtime != b->mtime) + return a->mtime - b->mtime; + return memcmp(a->obj->hash.h, b->obj->hash.h, sizeof(a->obj->hash.h)); +} + +static int +writeordercmp(void *pa, void *pb) +{ + Meta *a, *b, *ahd, *bhd; + + a = *(Meta**)pa; + b = *(Meta**)pb; + ahd = (a->head == nil) ? a : a->head; + bhd = (b->head == nil) ? b : b->head; + if(ahd->mtime != bhd->mtime) + return bhd->mtime - ahd->mtime; + if(ahd != bhd) + return (uintptr)bhd - (uintptr)ahd; + if(a->nchain != b->nchain) + return a->nchain - b->nchain; + return a->mtime - b->mtime; +} + +static void +addmeta(Metavec *v, Objset *has, Object *o, char *path, vlong mtime) +{ + Meta *m; + + if(oshas(has, o->hash)) + return; + osadd(has, o); + if(v == nil) + return; + m = emalloc(sizeof(Meta)); + m->obj = o; + m->path = estrdup(path); + m->mtime = mtime; + + if(v->nmeta == v->metasz){ + v->metasz = 2*v->metasz; + v->meta = earealloc(v->meta, v->metasz, sizeof(Meta*)); + } + v->meta[v->nmeta++] = m; +} + +static void +freemeta(Meta *m) +{ + free(m->delta); + free(m->path); + free(m); +} + +static int +loadtree(Metavec *v, Objset *has, Hash tree, char *dpath, vlong mtime) +{ + Object *t, *o; + Dirent *e; + char *p; + int i, k; + + if(oshas(has, tree)) + return 0; + if((t = readobject(tree)) == nil) + return -1; + if(t->type != GTree){ + fprint(2, "load: %H: not tree\n", t->hash); + unref(t); + return 0; + } + addmeta(v, has, t, dpath, mtime); + for(i = 0; i < t->tree->nent; i++){ + e = &t->tree->ent[i]; + if(oshas(has, e->h)) + continue; + if(e->ismod) + continue; + k = (e->mode & DMDIR) ? GTree : GBlob; + o = clearedobject(e->h, k); + p = smprint("%s/%s", dpath, e->name); + if(k == GBlob) + addmeta(v, has, o, p, mtime); + else if(loadtree(v, has, e->h, p, mtime) == -1){ + free(p); + return -1; + } + free(p); + } + unref(t); + return 0; +} + +static int +loadcommit(Metavec *v, Objset *has, Hash h) +{ + Object *c; + int r; + + if(osfind(has, h)) + return 0; + if((c = readobject(h)) == nil) + return -1; + if(c->type != GCommit){ + fprint(2, "load: %H: not commit\n", c->hash); + unref(c); + return 0; + } + addmeta(v, has, c, "", c->commit->ctime); + r = loadtree(v, has, c->commit->tree, "", c->commit->ctime); + unref(c); + return r; +} + +static int +readmeta(Hash *theirs, int ntheirs, Hash *ours, int nours, Meta ***m) +{ + Object **obj; + Objset has; + int i, nobj; + Metavec v; + + *m = nil; + osinit(&has); + v.nmeta = 0; + v.metasz = 64; + v.meta = eamalloc(v.metasz, sizeof(Meta*)); + if(findtwixt(theirs, ntheirs, ours, nours, &obj, &nobj) == -1) + sysfatal("load twixt: %r"); + + if(nobj == 0) + return 0; + for(i = 0; i < nours; i++) + if(!hasheq(&ours[i], &Zhash)) + if(loadcommit(nil, &has, ours[i]) == -1) + goto out; + for(i = 0; i < nobj; i++) + if(loadcommit(&v, &has, obj[i]->hash) == -1) + goto out; + osclear(&has); + *m = v.meta; + return v.nmeta; +out: + osclear(&has); + free(v.meta); + return -1; +} + +static int +deltasz(Delta *d, int nd) +{ + int i, sz; + sz = 32; + for(i = 0; i < nd; i++) + sz += d[i].cpy ? 7 : d[i].len + 1; + return sz; +} + +static void +pickdeltas(Meta **meta, int nmeta) +{ + Meta *m, *p; + Object *o; + Delta *d; + int i, j, nd, sz, pct, best; + + pct = 0; + dprint(1, "picking deltas\n"); + if(interactive) + fprint(2, "deltifying %d objects: 0%%", nmeta); + qsort(meta, nmeta, sizeof(Meta*), deltaordercmp); + for(i = 0; i < nmeta; i++){ + m = meta[i]; + pct = showprogress((i*100) / nmeta, pct); + m->delta = nil; + m->ndelta = 0; + if(m->obj->type == GCommit || m->obj->type == GTag) + continue; + if((o = readobject(m->obj->hash)) == nil) + sysfatal("readobject %H: %r", m->obj->hash); + dtinit(&m->dtab, o); + if(i >= 11) + dtclear(&meta[i-11]->dtab); + best = o->size; + for(j = max(0, i - 10); j < i; j++){ + p = meta[j]; + /* long chains make unpacking slow */ + if(p->nchain >= 128 || p->obj->type != o->type) + continue; + d = deltify(o, &p->dtab, &nd); + sz = deltasz(d, nd); + if(sz + 32 < best){ + /* + * if we already picked a best delta, + * replace it. + */ + free(m->delta); + best = sz; + m->delta = d; + m->ndelta = nd; + m->nchain = p->nchain + 1; + m->prev = p; + m->head = p->head; + if(m->head == nil) + m->head = p; + }else + free(d); + } + unref(o); + } + for(i = max(0, nmeta - 10); i < nmeta; i++) + dtclear(&meta[i]->dtab); + if(interactive) + fprint(2, "\b\b\b\b100%%\n"); +} + +static int +compread(void *p, void *dst, int n) +{ + Buf *b; + + b = p; + if(n > b->sz - b->off) + n = b->sz - b->off; + memcpy(dst, b->data + b->off, n); + b->off += n; + return n; +} + +static int +compwrite(void *p, void *buf, int n) +{ + return hwrite(((Compout *)p)->bfd, buf, n, &((Compout*)p)->st); +} + +static int +hcompress(Biobuf *bfd, void *buf, int sz, DigestState **st) +{ + int r; + Buf b ={ + .off=0, + .data=buf, + .sz=sz, + }; + Compout o = { + .bfd = bfd, + .st = *st, + }; + + r = deflatezlib(&o, compwrite, &b, compread, 6, 0); + *st = o.st; + return r; +} + +static void +append(char **p, int *len, int *sz, void *seg, int nseg) +{ + if(*len + nseg >= *sz){ + while(*len + nseg >= *sz) + *sz += *sz/2; + *p = erealloc(*p, *sz); + } + memcpy(*p + *len, seg, nseg); + *len += nseg; +} + +static int +encodedelta(Meta *m, Object *o, Object *b, void **pp) +{ + char *p, *bp, buf[16]; + int len, sz, n, i, j; + Delta *d; + + sz = 128; + len = 0; + p = emalloc(sz); + + /* base object size */ + buf[0] = b->size & 0x7f; + n = b->size >> 7; + for(i = 1; n > 0; i++){ + buf[i - 1] |= 0x80; + buf[i] = n & 0x7f; + n >>= 7; + } + append(&p, &len, &sz, buf, i); + + /* target object size */ + buf[0] = o->size & 0x7f; + n = o->size >> 7; + for(i = 1; n > 0; i++){ + buf[i - 1] |= 0x80; + buf[i] = n & 0x7f; + n >>= 7; + } + append(&p, &len, &sz, buf, i); + for(j = 0; j < m->ndelta; j++){ + d = &m->delta[j]; + if(d->cpy){ + n = d->off; + bp = buf + 1; + buf[0] = 0x81; + buf[1] = 0x00; + for(i = 0; i < sizeof(buf); i++) { + buf[0] |= 1<>= 8; + if(n == 0) + break; + } + + n = d->len; + if(n != 0x10000) { + buf[0] |= 0x1<<4; + for(i = 0; i < sizeof(buf)-4 && n > 0; i++){ + buf[0] |= 1<<(i + 4); + *bp++ = n & 0xff; + n >>= 8; + } + } + append(&p, &len, &sz, buf, bp - buf); + }else{ + n = 0; + while(n != d->len){ + buf[0] = (d->len - n < 127) ? d->len - n : 127; + append(&p, &len, &sz, buf, 1); + append(&p, &len, &sz, o->data + d->off + n, buf[0]); + n += buf[0]; + } + } + } + *pp = p; + return len; +} + +static int +packhdr(char *hdr, int ty, int len) +{ + int i; + + hdr[0] = ty << 4; + hdr[0] |= len & 0xf; + len >>= 4; + for(i = 1; len != 0; i++){ + hdr[i-1] |= 0x80; + hdr[i] = len & 0x7f; + len >>= 7; + } + return i; +} + +static int +packoff(char *hdr, vlong off) +{ + int i, j; + char rbuf[8]; + + rbuf[0] = off & 0x7f; + for(i = 1; (off >>= 7) != 0; i++) + rbuf[i] = (--off & 0x7f)|0x80; + + j = 0; + while(i > 0) + hdr[j++] = rbuf[--i]; + return j; +} + +static int +genpack(int fd, Meta **meta, int nmeta, Hash *h, int odelta) +{ + int i, nh, nd, res, pct, ret; + DigestState *st; + Biobuf *bfd; + Meta *m; + Object *o, *po, *b; + char *p, buf[32]; + + st = nil; + ret = -1; + pct = 0; + dprint(1, "generating pack\n"); + if((fd = dup(fd, -1)) == -1) + return -1; + if((bfd = Bfdopen(fd, OWRITE)) == nil) + return -1; + if(hwrite(bfd, "PACK", 4, &st) == -1) + return -1; + PUTBE32(buf, 2); + if(hwrite(bfd, buf, 4, &st) == -1) + return -1; + PUTBE32(buf, nmeta); + if(hwrite(bfd, buf, 4, &st) == -1) + return -1; + qsort(meta, nmeta, sizeof(Meta*), writeordercmp); + if(interactive) + fprint(2, "writing %d objects: 0%%", nmeta); + for(i = 0; i < nmeta; i++){ + pct = showprogress((i*100)/nmeta, pct); + m = meta[i]; + m->off = Boffset(bfd); + if(m->off == -1) + goto error; + if((o = readobject(m->obj->hash)) == nil) + return -1; + if(m->delta == nil){ + nh = packhdr(buf, o->type, o->size); + if(hwrite(bfd, buf, nh, &st) == -1) + goto error; + if(hcompress(bfd, o->data, o->size, &st) == -1) + goto error; + }else{ + if((b = readobject(m->prev->obj->hash)) == nil) + goto error; + nd = encodedelta(m, o, b, &p); + unref(b); + if(odelta && m->prev->off != 0){ + nh = 0; + nh += packhdr(buf, GOdelta, nd); + nh += packoff(buf+nh, m->off - m->prev->off); + if(hwrite(bfd, buf, nh, &st) == -1) + goto error; + }else{ + nh = packhdr(buf, GRdelta, nd); + po = m->prev->obj; + if(hwrite(bfd, buf, nh, &st) == -1) + goto error; + if(hwrite(bfd, po->hash.h, sizeof(po->hash.h), &st) == -1) + goto error; + } + res = hcompress(bfd, p, nd, &st); + free(p); + if(res == -1) + goto error; + } + unref(o); + } + if(interactive) + fprint(2, "\b\b\b\b100%%\n"); + sha1(nil, 0, h->h, st); + if(Bwrite(bfd, h->h, sizeof(h->h)) == -1) + goto error; + ret = 0; +error: + if(Bterm(bfd) == -1) + return -1; + return ret; +} + +int +writepack(int fd, Hash *theirs, int ntheirs, Hash *ours, int nours, Hash *h) +{ + Meta **meta; + int i, r, nmeta; + + if((nmeta = readmeta(theirs, ntheirs, ours, nours, &meta)) == -1) + return -1; + pickdeltas(meta, nmeta); + r = genpack(fd, meta, nmeta, h, 0); + for(i = 0; i < nmeta; i++) + freemeta(meta[i]); + free(meta); + return r; +} diff --git a/sys/src/cmd/git/proto.c b/sys/src/cmd/git/proto.c new file mode 100644 index 000000000..05d72cc49 --- /dev/null +++ b/sys/src/cmd/git/proto.c @@ -0,0 +1,514 @@ +#include +#include +#include + +#include "git.h" + +#define Useragent "useragent git/2.24.1" +#define Contenthdr "headers Content-Type: application/x-git-%s-pack-request" +#define Accepthdr "headers Accept: application/x-git-%s-pack-result" + +enum { + Nproto = 16, + Nport = 16, + Nhost = 256, + Npath = 128, + Nrepo = 64, + Nbranch = 32, +}; + +void +tracepkt(int v, char *pfx, char *b, int n) +{ + char *f; + int o, i; + + if(chattygit < v) + return; + o = 0; + f = emalloc(n*4 + 1); + for(i = 0; i < n; i++){ + if(isprint(b[i])){ + f[o++] = b[i]; + continue; + } + f[o++] = '\\'; + switch(b[i]){ + case '\\': f[o++] = '\\'; break; + case '\n': f[o++] = 'n'; break; + case '\r': f[o++] = 'r'; break; + case '\v': f[o++] = 'v'; break; + case '\0': f[o++] = '0'; break; + default: + f[o++] = 'x'; + f[o++] = "0123456789abcdef"[(b[i]>>4)&0xf]; + f[o++] = "0123456789abcdef"[(b[i]>>0)&0xf]; + break; + } + } + f[o] = '\0'; + fprint(2, "%s %04x:\t%s\n", pfx, n, f); + free(f); +} + +int +readpkt(Conn *c, char *buf, int nbuf) +{ + char len[5]; + char *e; + int n; + + if(readn(c->rfd, len, 4) == -1) + return -1; + len[4] = 0; + n = strtol(len, &e, 16); + if(n == 0){ + dprint(1, "=r=> 0000\n"); + return 0; + } + if(e != len + 4 || n <= 4) + sysfatal("pktline: bad length '%s'", len); + n -= 4; + if(n >= nbuf) + sysfatal("pktline: undersize buffer"); + if(readn(c->rfd, buf, n) != n) + return -1; + buf[n] = 0; + tracepkt(1, "=r=>", buf, n); + return n; +} + +int +writepkt(Conn *c, char *buf, int nbuf) +{ + char len[5]; + + + snprint(len, sizeof(len), "%04x", nbuf + 4); + if(write(c->wfd, len, 4) != 4) + return -1; + if(write(c->wfd, buf, nbuf) != nbuf) + return -1; + tracepkt(1, "<=w=", buf, nbuf); + return 0; +} + +int +flushpkt(Conn *c) +{ + dprint(1, "<=w= 0000\n"); + return write(c->wfd, "0000", 4); +} + +static void +grab(char *dst, int n, char *p, char *e) +{ + int l; + + l = e - p; + if(l >= n) + sysfatal("overlong component"); + memcpy(dst, p, l); + dst[l] = 0; +} + +static int +parseuri(char *uri, char *proto, char *host, char *port, char *path, char *repo) +{ + char *s, *p, *q; + int n, hasport; + print("uri: \"%s\"\n", uri); + + p = strstr(uri, "://"); + if(p == nil) + snprint(proto, Nproto, "ssh"); + else if(strncmp(uri, "git+", 4) == 0) + grab(proto, Nproto, uri + 4, p); + else + grab(proto, Nproto, uri, p); + *port = 0; + hasport = 1; + if(strcmp(proto, "git") == 0) + snprint(port, Nport, "9418"); + else if(strncmp(proto, "https", 5) == 0) + snprint(port, Nport, "443"); + else if(strncmp(proto, "http", 4) == 0) + snprint(port, Nport, "80"); + else if(strncmp(proto, "hjgit", 5) == 0) + snprint(port, Nport, "17021"); + else if(strncmp(proto, "gits", 5) == 0) + snprint(port, Nport, "9419"); + else + hasport = 0; + s = (p != nil) ? p + 3 : uri; + p = nil; + if(!hasport){ + p = strstr(s, ":"); + if(p != nil) + p++; + } + if(p == nil) + p = strstr(s, "/"); + if(p == nil || strlen(p) == 1){ + werrstr("missing path"); + return -1; + } + + q = memchr(s, ':', p - s); + if(q){ + grab(host, Nhost, s, q); + grab(port, Nport, q + 1, p); + }else{ + grab(host, Nhost, s, p); + } + + snprint(path, Npath, "%s", p); + if((q = strrchr(p, '/')) != nil) + p = q + 1; + if(strlen(p) == 0){ + werrstr("missing repository in uri"); + return -1; + } + n = strlen(p); + if(hassuffix(p, ".git")) + n -= 4; + grab(repo, Nrepo, p, p + n); + return 0; +} + +static int +webclone(Conn *c, char *url) +{ + char buf[16]; + int n, conn; + + if((c->cfd = open("/mnt/web/clone", ORDWR)) < 0) + goto err; + if((n = read(c->cfd, buf, sizeof(buf)-1)) == -1) + goto err; + buf[n] = 0; + conn = atoi(buf); + + /* github will behave differently based on useragent */ + if(write(c->cfd, Useragent, sizeof(Useragent)) == -1) + return -1; + dprint(1, "open url %s\n", url); + if(fprint(c->cfd, "url %s", url) == -1) + goto err; + free(c->dir); + c->dir = smprint("/mnt/web/%d", conn); + return 0; +err: + if(c->cfd != -1) + close(c->cfd); + return -1; +} + +static int +webopen(Conn *c, char *file, int mode) +{ + char path[128]; + int fd; + + snprint(path, sizeof(path), "%s/%s", c->dir, file); + if((fd = open(path, mode)) == -1) + return -1; + return fd; +} + +static int +issmarthttp(Conn *c, char *direction) +{ + char buf[Pktmax+1], svc[128]; + int fd, n; + + if((fd = webopen(c, "contenttype", OREAD)) == -1) + return -1; + n = readn(fd, buf, sizeof(buf) - 1); + close(fd); + if(n == -1) + return -1; + buf[n] = '\0'; + snprint(svc, sizeof(svc), "application/x-git-%s-pack-advertisement", direction); + if(strcmp(svc, buf) != 0){ + werrstr("dumb http protocol not supported"); + return -1; + } + + if((n = readpkt(c, buf, sizeof(buf))) == -1) + sysfatal("http read: %r"); + buf[n] = 0; + snprint(svc, sizeof(svc), "# service=git-%s-pack\n", direction); + if(strncmp(svc, buf, n) != 0){ + werrstr("invalid initial packet line"); + return -1; + } + if(readpkt(c, buf, sizeof(buf)) != 0){ + werrstr("protocol garble: expected flushpkt"); + return -1; + } + return 0; +} + +static int +dialhttp(Conn *c, char *host, char *port, char *path, char *direction) +{ + char *geturl, *suff, *hsep, *psep; + + suff = ""; + hsep = ""; + psep = ""; + if(port && strlen(port) != 0) + hsep = ":"; + if(path && path[0] != '/') + psep = "/"; + memset(c, 0, sizeof(*c)); + geturl = smprint("https://%s%s%s%s%s%s/info/refs?service=git-%s-pack", host, hsep, port, psep, path, suff, direction); + c->type = ConnHttp; + c->url = smprint("https://%s%s%s%s%s%s/git-%s-pack", host, hsep, port, psep, path, suff, direction); + c->cfd = webclone(c, geturl); + free(geturl); + if(c->cfd == -1) + return -1; + c->rfd = webopen(c, "body", OREAD); + c->wfd = -1; + if(c->rfd == -1) + return -1; + if(issmarthttp(c, direction) == -1) + return -1; + c->direction = estrdup(direction); + return 0; +} + +static int +dialssh(Conn *c, char *host, char *, char *path, char *direction) +{ + int pid, pfd[2]; + char cmd[64]; + + if(pipe(pfd) == -1) + sysfatal("unable to open pipe: %r"); + pid = fork(); + if(pid == -1) + sysfatal("unable to fork"); + if(pid == 0){ + close(pfd[1]); + dup(pfd[0], 0); + dup(pfd[0], 1); + snprint(cmd, sizeof(cmd), "git-%s-pack", direction); + dprint(1, "exec ssh '%s' '%s' %s\n", host, cmd, path); + execl("/bin/ssh", "ssh", host, cmd, path, nil); + sysfatal("exec: %r"); + } + close(pfd[0]); + c->type = ConnSsh; + c->rfd = pfd[1]; + c->wfd = dup(pfd[1], -1); + return 0; +} + +static int +githandshake(Conn *c, char *host, char *path, char *direction) +{ + char *p, *e, cmd[512]; + + p = cmd; + e = cmd + sizeof(cmd); + p = seprint(p, e - 1, "git-%s-pack %s", direction, path); + if(host != nil) + p = seprint(p + 1, e, "host=%s", host); + if(writepkt(c, cmd, p - cmd + 1) == -1){ + fprint(2, "failed to write message\n"); + closeconn(c); + return -1; + } + return 0; +} + +static int +dialhjgit(Conn *c, char *host, char *port, char *path, char *direction, int auth) +{ + char *ds; + int pid, pfd[2]; + + if((ds = netmkaddr(host, "tcp", port)) == nil) + return -1; + if(pipe(pfd) == -1) + sysfatal("unable to open pipe: %r"); + pid = fork(); + if(pid == -1) + sysfatal("unable to fork"); + if(pid == 0){ + close(pfd[1]); + dup(pfd[0], 0); + dup(pfd[0], 1); + dprint(1, "exec tlsclient -a %s\n", ds); + if(auth) + execl("/bin/tlsclient", "tlsclient", "-a", ds, nil); + else + execl("/bin/tlsclient", "tlsclient", ds, nil); + sysfatal("exec: %r"); + } + close(pfd[0]); + c->type = ConnGit9; + c->rfd = pfd[1]; + c->wfd = dup(pfd[1], -1); + return githandshake(c, host, path, direction); +} + +void +initconn(Conn *c, int rd, int wr) +{ + c->type = ConnGit; + c->rfd = rd; + c->wfd = wr; +} + +static int +dialgit(Conn *c, char *host, char *port, char *path, char *direction) +{ + char *ds; + int fd; + + if((ds = netmkaddr(host, "tcp", port)) == nil) + return -1; + dprint(1, "dial %s git-%s-pack %s\n", ds, direction, path); + fd = dial(ds, nil, nil, nil); + if(fd == -1) + return -1; + c->type = ConnGit; + c->rfd = fd; + c->wfd = dup(fd, -1); + return githandshake(c, host, path, direction); +} + +static int +servelocal(Conn *c, char *path, char *direction) +{ + int pid, pfd[2]; + + if(pipe(pfd) == -1) + sysfatal("unable to open pipe: %r"); + pid = fork(); + if(pid == -1) + sysfatal("unable to fork"); + if(pid == 0){ + close(pfd[1]); + dup(pfd[0], 0); + dup(pfd[0], 1); + execl("/bin/git/serve", "serve", "-w", nil); + sysfatal("exec: %r"); + } + close(pfd[0]); + c->type = ConnGit; + c->rfd = pfd[1]; + c->wfd = dup(pfd[1], -1); + return githandshake(c, nil, path, direction); +} + +static int +localrepo(char *uri, char *path, int npath) +{ + int fd; + + snprint(path, npath, "%s/.git/../", uri); + fd = open(path, OREAD); + if(fd < 0) + return -1; + if(fd2path(fd, path, npath) != 0){ + close(fd); + return -1; + } + close(fd); + return 0; +} + +int +gitconnect(Conn *c, char *uri, char *direction) +{ + char proto[Nproto], host[Nhost], port[Nport]; + char repo[Nrepo], path[Npath]; + + memset(c, 0, sizeof(Conn)); + c->rfd = c->wfd = c->cfd = -1; + + if(localrepo(uri, path, sizeof(path)) == 0) + return servelocal(c, path, direction); + + if(parseuri(uri, proto, host, port, path, repo) == -1){ + werrstr("bad uri %s", uri); + return -1; + } + if(strcmp(proto, "ssh") == 0) + return dialssh(c, host, port, path, direction); + else if(strcmp(proto, "git") == 0) + return dialgit(c, host, port, path, direction); + else if(strcmp(proto, "hjgit") == 0) + return dialhjgit(c, host, port, path, direction, 1); + else if(strcmp(proto, "gits") == 0) + return dialhjgit(c, host, port, path, direction, 0); + else if(strcmp(proto, "http") == 0 || strcmp(proto, "https") == 0) + return dialhttp(c, host, port, path, direction); + werrstr("unknown protocol %s", proto); + return -1; +} + +int +writephase(Conn *c) +{ + char hdr[128]; + int n; + + dprint(1, "start write phase\n"); + if(c->type != ConnHttp) + return 0; + + if(c->wfd != -1) + close(c->wfd); + if(c->cfd != -1) + close(c->cfd); + if((c->cfd = webclone(c, c->url)) == -1) + return -1; + n = snprint(hdr, sizeof(hdr), Contenthdr, c->direction); + if(write(c->cfd, hdr, n) == -1) + return -1; + n = snprint(hdr, sizeof(hdr), Accepthdr, c->direction); + if(write(c->cfd, hdr, n) == -1) + return -1; + if((c->wfd = webopen(c, "postbody", OWRITE)) == -1) + return -1; + c->rfd = -1; + return 0; +} + +int +readphase(Conn *c) +{ + dprint(1, "start read phase\n"); + if(c->type != ConnHttp) + return 0; + if(close(c->wfd) == -1) + return -1; + if((c->rfd = webopen(c, "body", OREAD)) == -1) + return -1; + c->wfd = -1; + return 0; +} + +void +closeconn(Conn *c) +{ + close(c->rfd); + close(c->wfd); + switch(c->type){ + case ConnGit: + break; + case ConnGit9: + case ConnSsh: + free(wait()); + break; + case ConnHttp: + close(c->cfd); + break; + } +} diff --git a/sys/src/cmd/git/pull b/sys/src/cmd/git/pull new file mode 100755 index 000000000..13db8151b --- /dev/null +++ b/sys/src/cmd/git/pull @@ -0,0 +1,73 @@ +#!/bin/rc -e +rfork en +. /sys/lib/git/common.rc + +fn update{ + upstream=$1 + url=$2 + dir=$3 + dflag=() + if(! ~ $#debug 0) + dflag='-d' + {git/get $dflag -u $upstream $url >[2=3] || die $status} | awk ' + /^remote/{ + if($2=="HEAD") + next + ref=$2 + hash=$3 + gsub("^refs/heads", "refs/remotes/'$upstream'", ref) + outfile = ".git/"ref + system("mkdir -p `{basename -d "outfile"}"); + print hash > outfile; + close(outfile); + } + ' |[3] tr '\x0d' '\x0a' +} + +gitup + +flagfmt='d:debug, q:quiet, f:fetchonly, + u:upstream upstream' +args='' +eval `''{aux/getflags $*} || exec aux/usage + +if(~ $#upstream 0) + upstream=origin +remote=`$nl{git/conf 'remote "'$upstream'".url'} +if(~ $#remote 0){ + remote=$upstream + upstream=THEM +} + +update $upstream $remote +if (~ $fetchonly 1) + exit + +local=`{git/branch} +remote=`{git/branch | subst '^(refs/)?heads' 'remotes/'$upstream} + +# we have local commits, but the remote hasn't changed. +# in this case, we want to keep the local commits untouched. +if(~ `{git/query HEAD $remote @} `{git/query $remote}){ + echo 'up to date' >[1=2] + exit +} +# The remote repository and our HEAD have diverged: we +# need to merge. +if(! ~ `{git/query HEAD $remote @} `{git/query HEAD}){ + >[1=2]{ + echo ours: `{git/query HEAD} + echo theirs: `{git/query $remote} + echo common: `{git/query HEAD $remote @} + echo git/merge $remote + } + exit diverged +} +# The remote is directly ahead of the local, and we have +# no local commits that need merging. +if(~ $#quiet 0) + git/log -s -e $local'..'$remote +echo +echo $remote':' `{git/query $local} '=>' `{git/query $remote} +git/branch -mnb $remote $local +exit '' diff --git a/sys/src/cmd/git/push b/sys/src/cmd/git/push new file mode 100755 index 000000000..ee05d952a --- /dev/null +++ b/sys/src/cmd/git/push @@ -0,0 +1,51 @@ +#!/bin/rc -e +rfork en +. /sys/lib/git/common.rc + +gitup + +flagfmt='a:pushall, b:branch branch, f:force, d:debug, + r:remove remove, u:upstream upstream' args='' +eval `''{aux/getflags $*} || exec aux/usage +if(! ~ $#* 0) + exec aux/usage + +if(~ $pushall 1) + branch=`$nl{cd .git/refs/heads && walk -f} +if(~ $#branch 0) + branch=`{git/branch} +if(~ $#branch 0) + die 'no branches' +if(~ $force 1) + force=-f +if(~ $debug 1) + debug='-d' + +if(~ $#upstream 0) + upstream=origin + +remotes=`$nl{git/conf -a 'remote "'$upstream'".url'} +if(~ $#remotes 0) + remotes=$upstream +branch=-b^$branch +if(! ~ $#remove 0) + remove=-r^$remove +for(remote in $remotes){ + updates=`$nl{git/send $debug $force $branch $remove $remote} || die $status + for(ln in $updates){ + u=`{echo $ln} + refpath=`{echo $u(2) | subst '^refs/heads/' '.git/refs/remotes/'$upstream'/'} + switch($u(1)){ + case update; + mkdir -p `{basename -d $refpath} + echo $u(4) > $refpath + echo $u(2)^':' $u(3) '=>' $u(4) + case delete; + echo $u(2)^': removed' + rm -f $refpath + case uptodate; + echo $u(2)^': up to date' + } + } +} +exit '' diff --git a/sys/src/cmd/git/query.c b/sys/src/cmd/git/query.c new file mode 100644 index 000000000..40020eafc --- /dev/null +++ b/sys/src/cmd/git/query.c @@ -0,0 +1,199 @@ +#include +#include + +#include "git.h" + +#pragma varargck type "P" void + +int fullpath; +int changes; +int reverse; +char *path[128]; +int npath; + +int +Pfmt(Fmt *f) +{ + int i, n; + + n = 0; + for(i = 0; i < npath; i++) + n += fmtprint(f, "%s/", path[i]); + return n; +} + +void +showdir(Hash dh, char *dname, char m) +{ + Dirent *p, *e; + Object *d; + + + path[npath++] = dname; + if((d = readobject(dh)) == nil) + sysfatal("bad hash %H", dh); + assert(d->type == GTree); + p = d->tree->ent; + e = p + d->tree->nent; + for(; p != e; p++){ + if(p->ismod) + continue; + if(p->mode & DMDIR) + showdir(p->h, p->name, m); + else + print("%c %P%s\n", m, p->name); + } + print("%c %P\n", m); + unref(d); + npath--; +} + +void +show(Dirent *e, char m) +{ + if(e->mode & DMDIR) + showdir(e->h, e->name, m); + else + print("%c %P%s\n", m, e->name); +} + +void +difftrees(Object *a, Object *b) +{ + Dirent *ap, *bp, *ae, *be; + int c; + + ap = ae = nil; + bp = be = nil; + if(a != nil){ + if(a->type != GTree) + return; + ap = a->tree->ent; + ae = ap + a->tree->nent; + } + if(b != nil){ + if(b->type != GTree) + return; + bp = b->tree->ent; + be = bp + b->tree->nent; + } + while(ap != ae && bp != be){ + c = strcmp(ap->name, bp->name); + if(c == 0){ + if(ap->mode == bp->mode && hasheq(&ap->h, &bp->h)) + goto next; + if(ap->mode != bp->mode) + print("! %P%s\n", ap->name); + else if(!(ap->mode & DMDIR) || !(bp->mode & DMDIR)) + print("@ %P%s\n", ap->name); + if((ap->mode & DMDIR) && (bp->mode & DMDIR)){ + if(npath >= nelem(path)) + sysfatal("path too deep"); + path[npath++] = ap->name; + if((a = readobject(ap->h)) == nil) + sysfatal("bad hash %H", ap->h); + if((b = readobject(bp->h)) == nil) + sysfatal("bad hash %H", bp->h); + difftrees(a, b); + unref(a); + unref(b); + npath--; + } +next: + ap++; + bp++; + }else if(c < 0) { + show(ap, '-'); + ap++; + }else if(c > 0){ + show(bp, '+'); + bp++; + } + } + for(; ap != ae; ap++) + show(ap, '-'); + for(; bp != be; bp++) + show(bp, '+'); +} + +void +diffcommits(Hash ah, Hash bh) +{ + Object *a, *b, *at, *bt; + + at = nil; + bt = nil; + if(!hasheq(&ah, &Zhash) && (a = readobject(ah)) != nil){ + if(a->type != GCommit) + sysfatal("not commit: %H", ah); + if((at = readobject(a->commit->tree)) == nil) + sysfatal("bad hash %H", a->commit->tree); + unref(a); + } + if(!hasheq(&bh, &Zhash) && (b = readobject(bh)) != nil){ + if(b->type != GCommit) + sysfatal("not commit: %H", ah); + if((bt = readobject(b->commit->tree)) == nil) + sysfatal("bad hash %H", b->commit->tree); + unref(b); + } + difftrees(at, bt); + unref(at); + unref(bt); +} + +void +usage(void) +{ + fprint(2, "usage: %s [-pcr] query\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + int i, j, n; + Hash *h; + char *p, *e, *s, *objpfx; + char query[2048], repo[512]; + + ARGBEGIN{ + case 'd': chattygit++; break; + case 'p': fullpath++; break; + case 'c': changes++; break; + case 'r': reverse ^= 1; break; + default: usage(); break; + }ARGEND; + + gitinit(); + fmtinstall('P', Pfmt); + + if(argc == 0) + usage(); + if(findrepo(repo, sizeof(repo)) == -1) + sysfatal("find root: %r"); + if(chdir(repo) == -1) + sysfatal("chdir: %r"); + if((objpfx = smprint("%s/.git/fs/object/", repo)) == nil) + sysfatal("smprint: %r"); + s = ""; + p = query; + e = query + nelem(query); + for(i = 0; i < argc; i++){ + p = seprint(p, e, "%s%s", s, argv[i]); + s = " "; + } + if((n = resolverefs(&h, query)) == -1) + sysfatal("resolve: %r"); + if(changes){ + if(n != 2) + sysfatal("diff: need 2 commits, got %d", n); + diffcommits(h[0], h[1]); + }else{ + p = (fullpath ? objpfx : ""); + for(j = 0; j < n; j++) + print("%s%H\n", p, h[reverse ? n - 1 - j : j]); + } + exits(nil); +} + diff --git a/sys/src/cmd/git/rebase b/sys/src/cmd/git/rebase new file mode 100755 index 000000000..395f3e693 --- /dev/null +++ b/sys/src/cmd/git/rebase @@ -0,0 +1,92 @@ +#!/bin/rc + +. /sys/lib/git/common.rc +gitup + +flagfmt='a:abort, r:resume, i:interactive'; args='onto' +eval `''{aux/getflags $*} || exec aux/usage + +tmp=_rebase.working +if(! git/walk -q) + die dirty working tree +if(~ $#abort 1){ + if(! test -f .git/rebase.todo) + die no rebase to abort + src=`{cat .git/rebase.src} + rm -f .git/rebase.^(src todo) + git/branch $src + git/branch -d $tmp + exit +} +if(test -f .git/rebase.todo){ + if(~ $#resume 0) + die rebase in progress + if(! ~ $#* 0) + exec aux/usage + src=`{cat .git/rebase.src} +} +if not{ + if(! ~ $#* 1) + exec aux/usage + src=`{git/branch} + dst=`{git/query $1} + echo $src > .git/rebase.src + git/log -se $dst' '$src' @ .. '$src | sed 's/^/pick /' >.git/rebase.todo + if(! ~ $#interactive 0){ + giteditor=`{git/conf core.editor} + if(~ $#editor 0) + editor=$giteditor + if(~ $#editor 0) + editor=hold + $editor .git/rebase.todo + } + git/branch -nb $dst $tmp +} +todo=`$nl{cat .git/rebase.todo} + +fn sigexit { + s=$status + if(!) + echo 'fix and git/rebase -r' + >.git/rebase.todo for(i in $todo) + echo $i + status=$s +} + +flag e + + +while(! ~ $#todo 0){ + item=`{echo $todo(1)} + todo=$todo(2-) + echo $item + c=$item(2) + switch($item(1)){ + case p pick + git/export $c | git/import + case r reword + git/export $c | git/import + git/commit -re + case e edit + git/export $c | git/import + echo 'stopped for edit, resume with git/rebase -r' + exit + case s squash + git/export $c | git/import -n + msg=`''{cat $gitfs/HEAD/msg; echo; cat $gitfs/object/$c/msg} + git/commit -rem $msg . + case f fixup + git/export $c | git/import -n + git/commit -r . + case b break + echo 'stopped, resume with git/rebase -r' + exit + case '#'* '' + case * + die 'unknown command '''^$item(1)^'''' + } +} + +fn sigexit +git/branch -nb $tmp $src +git/branch -d $tmp +rm .git/rebase.todo .git/rebase.src diff --git a/sys/src/cmd/git/ref.c b/sys/src/cmd/git/ref.c new file mode 100644 index 000000000..11eea7a07 --- /dev/null +++ b/sys/src/cmd/git/ref.c @@ -0,0 +1,567 @@ +#include +#include +#include + +#include "git.h" + +typedef struct Eval Eval; + +enum { + Blank, + Keep, + Drop, + Skip, +}; + +struct Eval { + char *str; + char *p; + Object **stk; + int nstk; + int stksz; +}; + +static char *colors[] = { +[Keep] "keep", +[Drop] "drop", +[Blank] "blank", +[Skip] "skip", +}; + +static Object zcommit = { + .type=GCommit +}; + +void +eatspace(Eval *ev) +{ + while(isspace(ev->p[0])) + ev->p++; +} + +void +push(Eval *ev, Object *o) +{ + if(ev->nstk == ev->stksz){ + ev->stksz = 2*ev->stksz + 1; + ev->stk = erealloc(ev->stk, ev->stksz*sizeof(Object*)); + } + ev->stk[ev->nstk++] = o; +} + +Object* +pop(Eval *ev) +{ + if(ev->nstk == 0) + sysfatal("stack underflow"); + return ev->stk[--ev->nstk]; +} + +Object* +peek(Eval *ev) +{ + if(ev->nstk == 0) + sysfatal("stack underflow"); + return ev->stk[ev->nstk - 1]; +} + +int +isword(char e) +{ + return isalnum(e) || e == '/' || e == '-' || e == '_' || e == '.'; +} + +int +word(Eval *ev, char *b, int nb) +{ + char *p, *e; + int n; + + p = ev->p; + for(e = p; isword(*e) && strncmp(e, "..", 2) != 0; e++) + /* nothing */; + /* 1 for nul terminator */ + n = e - p + 1; + if(n >= nb) + n = nb; + snprint(b, n, "%s", p); + ev->p = e; + return n > 0; +} + +int +take(Eval *ev, char *m) +{ + int l; + + l = strlen(m); + if(strncmp(ev->p, m, l) != 0) + return 0; + ev->p += l; + return 1; +} + +static int +paint(Hash *head, int nhead, Hash *tail, int ntail, Object ***res, int *nres, int ancestor) +{ + Qelt e; + Objq objq; + Objset keep, drop, skip; + Object *o, *c; + int i, nskip; + + osinit(&keep); + osinit(&drop); + osinit(&skip); + qinit(&objq); + nskip = 0; + + for(i = 0; i < nhead; i++){ + if((o = readobject(head[i])) == nil){ + fprint(2, "warning: %H does not point at commit\n", o->hash); + werrstr("read head %H: %r", head[i]); + return -1; + } + if(o->type != GCommit){ + fprint(2, "warning: %H does not point at commit\n", o->hash); + unref(o); + continue; + } + dprint(1, "init: keep %H\n", o->hash); + qput(&objq, o, Keep); + unref(o); + } + for(i = 0; i < ntail; i++){ + if((o = readobject(tail[i])) == nil){ + werrstr("read tail %H: %r", tail[i]); + return -1; + } + if(o->type != GCommit){ + fprint(2, "warning: %H does not point at commit\n", o->hash); + unref(o); + continue; + } + dprint(1, "init: drop %H\n", o->hash); + qput(&objq, o, Drop); + unref(o); + } + + dprint(1, "finding twixt commits\n"); + while(nskip != objq.nheap && qpop(&objq, &e)){ + if(e.color == Skip) + nskip--; + if(oshas(&skip, e.o->hash)) + continue; + switch(e.color){ + case Keep: + if(oshas(&keep, e.o->hash)) + continue; + if(oshas(&drop, e.o->hash)) + e.color = Skip; + osadd(&keep, e.o); + break; + case Drop: + if(oshas(&drop, e.o->hash)) + continue; + if(oshas(&keep, e.o->hash)) + e.color = Skip; + osadd(&drop, e.o); + break; + case Skip: + osadd(&skip, e.o); + break; + } + o = readobject(e.o->hash); + for(i = 0; i < o->commit->nparent; i++){ + if((c = readobject(e.o->commit->parent[i])) == nil) + goto error; + if(c->type != GCommit){ + fprint(2, "warning: %H does not point at commit\n", c->hash); + unref(c); + continue; + } + dprint(2, "\tenqueue: %s %H\n", colors[e.color], c->hash); + qput(&objq, c, e.color); + unref(c); + if(e.color == Skip) + nskip++; + } + unref(o); + } + if(ancestor){ + dprint(1, "found ancestor\n"); + o = nil; + for(i = 0; i < keep.sz; i++){ + o = keep.obj[i]; + if(o != nil && oshas(&drop, o->hash) && !oshas(&skip, o->hash)) + break; + } + if(i == keep.sz){ + *nres = 0; + *res = nil; + }else{ + *nres = 1; + *res = eamalloc(1, sizeof(Object*)); + (*res)[0] = o; + } + }else{ + dprint(1, "found twixt\n"); + *res = eamalloc(keep.nobj, sizeof(Object*)); + *nres = 0; + for(i = 0; i < keep.sz; i++){ + o = keep.obj[i]; + if(o != nil && !oshas(&drop, o->hash) && !oshas(&skip, o->hash)){ + (*res)[*nres] = o; + (*nres)++; + } + } + } + osclear(&keep); + osclear(&drop); + osclear(&skip); + return 0; +error: + dprint(1, "twixt error: %r\n"); + free(objq.heap); + return -1; +} + +int +findtwixt(Hash *head, int nhead, Hash *tail, int ntail, Object ***res, int *nres) +{ + return paint(head, nhead, tail, ntail, res, nres, 0); +} + +Object* +ancestor(Object *a, Object *b) +{ + Object **o, *r; + int n; + + if(paint(&a->hash, 1, &b->hash, 1, &o, &n, 1) == -1 || n == 0) + return nil; + r = ref(o[0]); + free(o); + return r; +} + +int +lca(Eval *ev) +{ + Object *a, *b, **o; + int n; + + if(ev->nstk < 2){ + werrstr("ancestor needs 2 objects"); + return -1; + } + n = 0; + b = pop(ev); + a = pop(ev); + paint(&a->hash, 1, &b->hash, 1, &o, &n, 1); + if(n == 0) + return -1; + push(ev, *o); + free(o); + return 0; +} + +static int +parent(Eval *ev) +{ + Object *o, *p; + + o = pop(ev); + /* Special case: first commit has no parent. */ + if(o->commit->nparent == 0) + p = emptydir(); + else if ((p = readobject(o->commit->parent[0])) == nil){ + werrstr("no parent for %H", o->hash); + return -1; + } + + push(ev, p); + return 0; +} + +static int +unwind(Eval *ev, Object **obj, int *idx, int nobj, Object **p, Objset *set, int keep) +{ + int i; + + for(i = nobj; i >= 0; i--){ + idx[i]++; + if(keep && !oshas(set, obj[i]->hash)){ + push(ev, obj[i]); + osadd(set, obj[i]); + }else{ + osadd(set, obj[i]); + } + if(idx[i] < obj[i]->commit->nparent){ + *p = obj[i]; + return i; + } + unref(obj[i]); + } + return -1; +} + +static int +range(Eval *ev) +{ + Object *a, *b, *p, *q, **all; + int nall, *idx; + Objset keep, skip; + + b = pop(ev); + a = pop(ev); + if(hasheq(&b->hash, &Zhash)) + b = &zcommit; + if(hasheq(&a->hash, &Zhash)) + a = &zcommit; + if(a->type != GCommit || b->type != GCommit){ + werrstr("non-commit object in range"); + return -1; + } + + p = b; + all = nil; + idx = nil; + nall = 0; + osinit(&keep); + osinit(&skip); + osadd(&keep, a); + while(1){ + all = earealloc(all, (nall + 1), sizeof(Object*)); + idx = earealloc(idx, (nall + 1), sizeof(int)); + all[nall] = p; + idx[nall] = 0; + if(p == a || p->commit->nparent == 0 && a == &zcommit){ + if((nall = unwind(ev, all, idx, nall, &p, &keep, 1)) == -1) + break; + }else if(p->commit->nparent == 0){ + if((nall = unwind(ev, all, idx, nall, &p, &skip, 0)) == -1) + break; + }else if(oshas(&keep, p->hash)){ + if((nall = unwind(ev, all, idx, nall, &p, &keep, 1)) == -1) + break; + }else if(oshas(&skip, p->hash)) + if((nall = unwind(ev, all, idx, nall, &p, &skip, 0)) == -1) + break; + if(p->commit->nparent == 0) + break; + if((q = readobject(p->commit->parent[idx[nall]])) == nil){ + werrstr("bad commit %H", p->commit->parent[idx[nall]]); + goto error; + } + if(q->type != GCommit){ + werrstr("not commit: %H", q->hash); + goto error; + } + p = q; + nall++; + } + free(all); + return 0; +error: + free(all); + return -1; +} + +int +readref(Hash *h, char *ref) +{ + static char *try[] = {"", "refs/", "refs/heads/", "refs/remotes/", "refs/tags/", nil}; + char buf[256], s[256], **pfx; + int r, f, n; + + /* TODO: support hash prefixes */ + if((r = hparse(h, ref)) != -1) + return r; + if(strcmp(ref, "HEAD") == 0){ + snprint(buf, sizeof(buf), ".git/HEAD"); + if((f = open(buf, OREAD)) == -1) + return -1; + if((n = readn(f, s, sizeof(s) - 1))== -1) + return -1; + s[n] = 0; + strip(s); + r = hparse(h, s); + goto found; + } + for(pfx = try; *pfx; pfx++){ + snprint(buf, sizeof(buf), ".git/%s%s", *pfx, ref); + if((f = open(buf, OREAD)) == -1) + continue; + if((n = readn(f, s, sizeof(s) - 1)) == -1) + continue; + s[n] = 0; + strip(s); + r = hparse(h, s); + close(f); + goto found; + } + return -1; + +found: + if(r == -1 && strstr(s, "ref: ") == s) + r = readref(h, s + strlen("ref: ")); + return r; +} + +int +evalpostfix(Eval *ev) +{ + char name[256]; + Object *o; + Hash h; + + eatspace(ev); + if(!word(ev, name, sizeof(name))){ + werrstr("expected name in expression"); + return -1; + } + if(readref(&h, name) == -1){ + werrstr("invalid ref %s", name); + return -1; + } + if(hasheq(&h, &Zhash)) + o = &zcommit; + else if((o = readobject(h)) == nil){ + werrstr("invalid ref %s (hash %H)", name, h); + return -1; + } + push(ev, o); + + while(1){ + eatspace(ev); + switch(ev->p[0]){ + case '^': + case '~': + ev->p++; + if(parent(ev) == -1) + return -1; + break; + case '@': + ev->p++; + if(lca(ev) == -1) + return -1; + break; + default: + goto done; + break; + } + } +done: + return 0; +} + +int +evalexpr(Eval *ev, char *ref) +{ + memset(ev, 0, sizeof(*ev)); + ev->str = ref; + ev->p = ref; + + while(1){ + if(evalpostfix(ev) == -1) + return -1; + if(ev->p[0] == '\0') + return 0; + else if(take(ev, ":") || take(ev, "..")){ + if(evalpostfix(ev) == -1) + return -1; + if(ev->p[0] != '\0'){ + werrstr("junk at end of expression"); + return -1; + } + return range(ev); + } + } +} + +int +resolverefs(Hash **r, char *ref) +{ + Eval ev; + Hash *h; + int i; + + if(evalexpr(&ev, ref) == -1){ + free(ev.stk); + return -1; + } + h = eamalloc(ev.nstk, sizeof(Hash)); + for(i = 0; i < ev.nstk; i++) + h[i] = ev.stk[i]->hash; + *r = h; + free(ev.stk); + return ev.nstk; +} + +int +resolveref(Hash *r, char *ref) +{ + Eval ev; + + if(evalexpr(&ev, ref) == -1){ + free(ev.stk); + return -1; + } + if(ev.nstk != 1){ + werrstr("ambiguous ref expr"); + free(ev.stk); + return -1; + } + *r = ev.stk[0]->hash; + free(ev.stk); + return 0; +} + +int +readrefdir(Hash **refs, char ***names, int *nrefs, char *dpath, char *dname) +{ + Dir *d, *e, *dir; + char *path, *name, *sep; + int ndir; + + if((ndir = slurpdir(dpath, &dir)) == -1) + return -1; + sep = (*dname == '\0') ? "" : "/"; + e = dir + ndir; + for(d = dir; d != e; d++){ + path = smprint("%s/%s", dpath, d->name); + name = smprint("%s%s%s", dname, sep, d->name); + if(d->mode & DMDIR) { + if(readrefdir(refs, names, nrefs, path, name) == -1) + goto noref; + }else{ + *refs = erealloc(*refs, (*nrefs + 1)*sizeof(Hash)); + *names = erealloc(*names, (*nrefs + 1)*sizeof(char*)); + if(resolveref(&(*refs)[*nrefs], name) == -1) + goto noref; + (*names)[*nrefs] = name; + *nrefs += 1; + goto next; + } +noref: free(name); +next: free(path); + } + free(dir); + return 0; +} + +int +listrefs(Hash **refs, char ***names) +{ + int nrefs; + + *refs = nil; + *names = nil; + nrefs = 0; + if(readrefdir(refs, names, &nrefs, ".git/refs", "") == -1){ + free(*refs); + return -1; + } + return nrefs; +} diff --git a/sys/src/cmd/git/repack.c b/sys/src/cmd/git/repack.c new file mode 100644 index 000000000..601231b01 --- /dev/null +++ b/sys/src/cmd/git/repack.c @@ -0,0 +1,85 @@ +#include +#include + +#include "git.h" + +#define TMPPATH(suff) (".git/objects/pack/repack."suff) + +int +cleanup(Hash h) +{ + char newpfx[42], dpath[256], fpath[256]; + int i, j, nd; + Dir *d; + + snprint(newpfx, sizeof(newpfx), "%H.", h); + for(i = 0; i < 256; i++){ + snprint(dpath, sizeof(dpath), ".git/objects/%02x", i); + if((nd = slurpdir(dpath, &d)) == -1) + continue; + for(j = 0; j < nd; j++){ + snprint(fpath, sizeof(fpath), ".git/objects/%02x/%s", i, d[j].name); + remove(fpath); + } + remove(dpath); + free(d); + } + snprint(dpath, sizeof(dpath), ".git/objects/pack"); + if((nd = slurpdir(dpath, &d)) == -1) + return -1; + for(i = 0; i < nd; i++){ + if(strncmp(d[i].name, newpfx, strlen(newpfx)) == 0) + continue; + snprint(fpath, sizeof(fpath), ".git/objects/pack/%s", d[i].name); + remove(fpath); + } + return 0; +} + +void +usage(void) +{ + fprint(2, "usage: %s [-d]\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char path[128], **names; + int fd, nrefs; + Hash *refs, h; + Dir rn; + + ARGBEGIN{ + case 'd': + chattygit++; + break; + default: + usage(); + }ARGEND; + + gitinit(); + refs = nil; + if((nrefs = listrefs(&refs, &names)) == -1) + sysfatal("load refs: %r"); + if((fd = create(TMPPATH("pack.tmp"), OWRITE, 0644)) == -1) + sysfatal("open %s: %r", TMPPATH("pack.tmp")); + if(writepack(fd, refs, nrefs, nil, 0, &h) == -1) + sysfatal("writepack: %r"); + if(indexpack(TMPPATH("pack.tmp"), TMPPATH("idx.tmp"), h) == -1) + sysfatal("indexpack: %r"); + close(fd); + + nulldir(&rn); + rn.name = path; + snprint(path, sizeof(path), "%H.pack", h); + if(dirwstat(TMPPATH("pack.tmp"), &rn) == -1) + sysfatal("rename pack: %r"); + snprint(path, sizeof(path), "%H.idx", h); + if(dirwstat(TMPPATH("idx.tmp"), &rn) == -1) + sysfatal("rename pack: %r"); + if(cleanup(h) == -1) + sysfatal("cleanup: %r"); + exits(nil); +} diff --git a/sys/src/cmd/git/revert b/sys/src/cmd/git/revert new file mode 100644 index 000000000..1adb0d2e6 --- /dev/null +++ b/sys/src/cmd/git/revert @@ -0,0 +1,22 @@ +#!/bin/rc +rfork en +. /sys/lib/git/common.rc + +gitup + +flagfmt='c:query query' args='file ...' +if (! eval `''{aux/getflags $*} || ~ $#* 0) + exec aux/usage + +commit=$gitfs/HEAD +if(~ $#query 1) + commit=`{git/query -p $query} + +files=`$nl{cleanname -d $gitrel $* | drop $gitroot} +for(f in `$nl{cd $commit/tree/ && walk -f ./$files}){ + mkdir -p `{basename -d $f} + cp -x -- $commit/tree/$f $f + touch $f + git/add $f +} +exit '' diff --git a/sys/src/cmd/git/rm b/sys/src/cmd/git/rm new file mode 100755 index 000000000..deaf3ae5a --- /dev/null +++ b/sys/src/cmd/git/rm @@ -0,0 +1,3 @@ +#!/bin/rc -e + +exec git/add -r $* diff --git a/sys/src/cmd/git/save.c b/sys/src/cmd/git/save.c new file mode 100644 index 000000000..08a1a50cf --- /dev/null +++ b/sys/src/cmd/git/save.c @@ -0,0 +1,434 @@ +#include +#include +#include "git.h" + +typedef struct Objbuf Objbuf; +struct Objbuf { + int off; + char *hdr; + int nhdr; + char *dat; + int ndat; +}; +enum { + Maxparents = 16, +}; + +char *authorname; +char *authoremail; +char *committername; +char *committeremail; +char *commitmsg; +Hash parents[Maxparents]; +int nparents; + +int +gitmode(Dirent *e) +{ + if(e->islink) + return 0120000; + else if(e->ismod) + return 0160000; + else if(e->mode & DMDIR) + return 0040000; + else if(e->mode & 0111) + return 0100755; + else + return 0100644; +} + +int +entcmp(void *pa, void *pb) +{ + char abuf[256], bbuf[256], *ae, *be; + Dirent *a, *b; + + a = pa; + b = pb; + /* + * If the files have the same name, they're equal. + * Otherwise, If they're trees, they sort as thoug + * there was a trailing slash. + * + * Wat. + */ + if(strcmp(a->name, b->name) == 0) + return 0; + + ae = seprint(abuf, abuf + sizeof(abuf) - 1, a->name); + be = seprint(bbuf, bbuf + sizeof(bbuf) - 1, b->name); + if(a->mode & DMDIR) + *ae = '/'; + if(b->mode & DMDIR) + *be = '/'; + return strcmp(abuf, bbuf); +} + +static int +bwrite(void *p, void *buf, int nbuf) +{ + return Bwrite(p, buf, nbuf); +} + +static int +objbytes(void *p, void *buf, int nbuf) +{ + Objbuf *b; + int r, n, o; + char *s; + + b = p; + n = 0; + if(b->off < b->nhdr){ + r = b->nhdr - b->off; + r = (nbuf < r) ? nbuf : r; + memcpy(buf, b->hdr, r); + b->off += r; + nbuf -= r; + n += r; + } + if(b->off < b->ndat + b->nhdr){ + s = buf; + o = b->off - b->nhdr; + r = b->ndat - o; + r = (nbuf < r) ? nbuf : r; + memcpy(s + n, b->dat + o, r); + b->off += r; + n += r; + } + return n; +} + +void +writeobj(Hash *h, char *hdr, int nhdr, char *dat, int ndat) +{ + Objbuf b = {.off=0, .hdr=hdr, .nhdr=nhdr, .dat=dat, .ndat=ndat}; + char s[64], o[256]; + SHA1state *st; + Biobuf *f; + int fd; + + st = sha1((uchar*)hdr, nhdr, nil, nil); + st = sha1((uchar*)dat, ndat, nil, st); + sha1(nil, 0, h->h, st); + + snprint(s, sizeof(s), "%H", *h); + fd = create(".git/objects", OREAD, DMDIR|0755); + close(fd); + snprint(o, sizeof(o), ".git/objects/%c%c", s[0], s[1]); + fd = create(o, OREAD, DMDIR | 0755); + close(fd); + snprint(o, sizeof(o), ".git/objects/%c%c/%s", s[0], s[1], s + 2); + if(readobject(*h) == nil){ + if((f = Bopen(o, OWRITE)) == nil) + sysfatal("could not open %s: %r", o); + if(deflatezlib(f, bwrite, &b, objbytes, 9, 0) == -1) + sysfatal("could not write %s: %r", o); + Bterm(f); + } +} + +int +writetree(Dirent *ent, int nent, Hash *h) +{ + char *t, *txt, *etxt, hdr[128]; + int nhdr, n; + Dirent *d, *p; + + t = emalloc((16+256+20) * nent); + txt = t; + etxt = t + (16+256+20) * nent; + + /* sqeeze out deleted entries */ + n = 0; + p = ent; + for(d = ent; d != ent + nent; d++) + if(d->name) + p[n++] = *d; + nent = n; + + qsort(ent, nent, sizeof(Dirent), entcmp); + for(d = ent; d != ent + nent; d++){ + if(strlen(d->name) >= 255) + sysfatal("overly long filename: %s", d->name); + t = seprint(t, etxt, "%o %s", gitmode(d), d->name) + 1; + memcpy(t, d->h.h, sizeof(d->h.h)); + t += sizeof(d->h.h); + } + nhdr = snprint(hdr, sizeof(hdr), "%T %lld", GTree, (vlong)(t - txt)) + 1; + writeobj(h, hdr, nhdr, txt, t - txt); + free(txt); + return nent; +} + +void +blobify(Dir *d, char *path, int *mode, Hash *bh) +{ + char h[64], *buf; + int f, nh; + + if((d->mode & DMDIR) != 0) + sysfatal("not file: %s", path); + *mode = d->mode; + nh = snprint(h, sizeof(h), "%T %lld", GBlob, d->length) + 1; + if((f = open(path, OREAD)) == -1) + sysfatal("could not open %s: %r", path); + buf = emalloc(d->length); + if(readn(f, buf, d->length) != d->length) + sysfatal("could not read blob %s: %r", path); + writeobj(bh, h, nh, buf, d->length); + free(buf); + close(f); +} + +int +tracked(char *path) +{ + char ipath[256]; + Dir *d; + + /* Explicitly removed. */ + snprint(ipath, sizeof(ipath), ".git/index9/removed/%s", path); + if(strstr(cleanname(ipath), ".git/index9/removed") != ipath) + sysfatal("path %s leaves index", ipath); + d = dirstat(ipath); + if(d != nil && d->qid.type != QTDIR){ + free(d); + return 0; + } + + /* Explicitly added. */ + snprint(ipath, sizeof(ipath), ".git/index9/tracked/%s", path); + if(strstr(cleanname(ipath), ".git/index9/tracked") != ipath) + sysfatal("path %s leaves index", ipath); + if(access(ipath, AEXIST) == 0) + return 1; + + return 0; +} + +int +pathelt(char *buf, int nbuf, char *p, int *isdir) +{ + char *b; + + b = buf; + if(*p == '/') + p++; + while(*p && *p != '/' && b != buf + nbuf) + *b++ = *p++; + *b = '\0'; + *isdir = (*p == '/'); + return b - buf; +} + +Dirent* +dirent(Dirent **ent, int *nent, char *name) +{ + Dirent *d; + + for(d = *ent; d != *ent + *nent; d++) + if(d->name && strcmp(d->name, name) == 0) + return d; + *nent += 1; + *ent = erealloc(*ent, *nent * sizeof(Dirent)); + d = *ent + (*nent - 1); + memset(d, 0, sizeof(*d)); + d->name = estrdup(name); + return d; +} + +int +treeify(Object *t, char **path, char **epath, int off, Hash *h) +{ + int r, n, ne, nsub, nent, isdir; + char **p, **ep; + char elt[256]; + Object **sub; + Dirent *e, *ent; + Dir *d; + + r = -1; + nsub = 0; + nent = t->tree->nent; + ent = eamalloc(nent, sizeof(*ent)); + sub = eamalloc((epath - path), sizeof(Object*)); + memcpy(ent, t->tree->ent, nent*sizeof(*ent)); + for(p = path; p != epath; p = ep){ + ne = pathelt(elt, sizeof(elt), *p + off, &isdir); + for(ep = p; ep != epath; ep++){ + if(strncmp(elt, *ep + off, ne) != 0) + break; + if((*ep)[off+ne] != '\0' && (*ep)[off+ne] != '/') + break; + } + e = dirent(&ent, &nent, elt); + if(e->islink) + sysfatal("symlinks may not be modified: %s", *path); + if(e->ismod) + sysfatal("submodules may not be modified: %s", *path); + if(isdir){ + e->mode = DMDIR | 0755; + sub[nsub] = readobject(e->h); + if(sub[nsub] == nil || sub[nsub]->type != GTree) + sub[nsub] = emptydir(); + /* + * if after processing deletions, a tree is empty, + * mark it for removal from the parent. + * + * Note, it is still written to the object store, + * but this is fine -- and ensures that an empty + * repository will continue to work. + */ + n = treeify(sub[nsub], p, ep, off + ne + 1, &e->h); + if(n == 0) + e->name = nil; + else if(n == -1) + goto err; + }else{ + d = dirstat(*p); + if(d != nil && tracked(*p)) + blobify(d, *p, &e->mode, &e->h); + else + e->name = nil; + free(d); + } + } + if(nent == 0){ + werrstr("%.*s: empty directory", off, *path); + goto err; + } + + r = writetree(ent, nent, h); +err: + free(sub); + return r; +} + + +void +mkcommit(Hash *c, vlong date, Hash tree) +{ + char *s, h[64]; + int ns, nh, i; + Fmt f; + + fmtstrinit(&f); + fmtprint(&f, "tree %H\n", tree); + for(i = 0; i < nparents; i++) + fmtprint(&f, "parent %H\n", parents[i]); + fmtprint(&f, "author %s <%s> %lld +0000\n", authorname, authoremail, date); + fmtprint(&f, "committer %s <%s> %lld +0000\n", committername, committeremail, date); + fmtprint(&f, "\n"); + fmtprint(&f, "%s", commitmsg); + s = fmtstrflush(&f); + + ns = strlen(s); + nh = snprint(h, sizeof(h), "%T %d", GCommit, ns) + 1; + writeobj(c, h, nh, s, ns); + free(s); +} + +Object* +findroot(void) +{ + Object *t, *c; + Hash h; + + if(resolveref(&h, "HEAD") == -1) + return emptydir(); + if((c = readobject(h)) == nil || c->type != GCommit) + sysfatal("could not read HEAD %H", h); + if((t = readobject(c->commit->tree)) == nil) + sysfatal("could not read tree for commit %H", h); + return t; +} + +void +usage(void) +{ + fprint(2, "usage: %s -n name -e email -m message -d date [files...]\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + Hash th, ch; + char *dstr, cwd[1024]; + int i, r, ncwd; + vlong date; + Object *t; + + gitinit(); + if(access(".git", AEXIST) != 0) + sysfatal("could not find git repo: %r"); + if(getwd(cwd, sizeof(cwd)) == nil) + sysfatal("getcwd: %r"); + dstr = nil; + date = time(nil); + ncwd = strlen(cwd); + + ARGBEGIN{ + case 'm': + commitmsg = EARGF(usage()); + break; + case 'n': + authorname = EARGF(usage()); + break; + case 'e': + authoremail = EARGF(usage()); + break; + case 'N': + committername = EARGF(usage()); + break; + case 'E': + committeremail = EARGF(usage()); + break; + case 'd': + dstr = EARGF(usage()); + break; + case 'p': + if(nparents >= Maxparents) + sysfatal("too many parents"); + if(resolveref(&parents[nparents++], EARGF(usage())) == -1) + sysfatal("invalid parent: %r"); + break; + default: + usage(); + break; + }ARGEND; + + if(commitmsg == nil) + sysfatal("missing message"); + if(authorname == nil) + sysfatal("missing name"); + if(authoremail == nil) + sysfatal("missing email"); + if((committername == nil) != (committeremail == nil)) + sysfatal("partially specified committer"); + if(committername == nil && committeremail == nil){ + committername = authorname; + committeremail = authoremail; + } + if(dstr){ + date=strtoll(dstr, &dstr, 10); + if(strlen(dstr) != 0) + sysfatal("could not parse date %s", dstr); + } + for(i = 0; i < argc; i++){ + cleanname(argv[i]); + if(*argv[i] == '/' && strncmp(argv[i], cwd, ncwd) == 0) + argv[i] += ncwd; + while(*argv[i] == '/') + argv[i]++; + } + + t = findroot(); + r = treeify(t, argv, argv + argc, 0, &th); + if(r == -1) + sysfatal("could not commit: %r\n"); + mkcommit(&ch, date, th); + print("%H\n", ch); + exits(nil); +} diff --git a/sys/src/cmd/git/send.c b/sys/src/cmd/git/send.c new file mode 100644 index 000000000..1188a71fa --- /dev/null +++ b/sys/src/cmd/git/send.c @@ -0,0 +1,313 @@ +#include +#include + +#include "git.h" + +typedef struct Capset Capset; +typedef struct Map Map; + +struct Capset { + int sideband; + int sideband64k; + int report; +}; + +struct Map { + char *ref; + Hash ours; + Hash theirs; +}; + +int sendall; +int force; +int nbranch; +char **branch; +char *removed[128]; +int nremoved; +int npacked; +int nsent; + +int +findref(char **r, int nr, char *ref) +{ + int i; + for(i = 0; i < nr; i++) + if(strcmp(r[i], ref) == 0) + return i; + return -1; +} + +int +findkey(Map *m, int nm, char *ref) +{ + int i; + for(i = 0; i < nm; i++) + if(strcmp(m[i].ref, ref) == 0) + return i; + return -1; +} + +int +readours(Hash **tailp, char ***refp) +{ + int nu, i, idx; + char *r, *pfx, **ref; + Hash *tail; + + if(sendall) + return listrefs(tailp, refp); + nu = 0; + tail = eamalloc((nremoved + nbranch), sizeof(Hash)); + ref = eamalloc((nremoved + nbranch), sizeof(char*)); + for(i = 0; i < nbranch; i++){ + ref[nu] = estrdup(branch[i]); + if(resolveref(&tail[nu], branch[i]) == -1) + sysfatal("broken branch %s", branch[i]); + nu++; + } + for(i = 0; i < nremoved; i++){ + pfx = "refs/heads/"; + if(strstr(removed[i], "heads/") == removed[i]) + pfx = "refs/"; + if(strstr(removed[i], "refs/heads/") == removed[i]) + pfx = ""; + if((r = smprint("%s%s", pfx, removed[i])) == nil) + sysfatal("smprint: %r"); + if((idx = findref(ref, nu, r)) == -1) + idx = nu++; + assert(idx < nremoved + nbranch); + memcpy(&tail[idx], &Zhash, sizeof(Hash)); + free(r); + } + dprint(1, "nu: %d\n", nu); + for(i = 0; i < nu; i++) + dprint(1, "update: %H %s\n", tail[i], ref[i]); + *tailp = tail; + *refp = ref; + return nu; +} + +char * +matchcap(char *s, char *cap, int full) +{ + if(strncmp(s, cap, strlen(cap)) == 0) + if(!full || strlen(s) == strlen(cap)) + return s + strlen(cap); + return nil; +} + +void +parsecaps(char *caps, Capset *cs) +{ + char *p, *n; + + for(p = caps; p != nil; p = n){ + n = strchr(p, ' '); + if(n != nil) + *n++ = 0; + if(matchcap(p, "report-status", 1) != nil) + cs->report = 1; + if(matchcap(p, "side-band", 1) != nil) + cs->sideband = 1; + if(matchcap(p, "side-band-64k", 1) != nil) + cs->sideband64k = 1; + } +} + +int +sendpack(Conn *c) +{ + int i, n, idx, nsp, send, first; + int nours, ntheirs, nmap; + char buf[Pktmax], *sp[3]; + Hash h, *theirs, *ours; + Object *a, *b, *p, *o; + char **refs; + Capset cs; + Map *map, *m; + + first = 1; + memset(&cs, 0, sizeof(Capset)); + nours = readours(&ours, &refs); + theirs = nil; + ntheirs = 0; + nmap = nours; + map = eamalloc(nmap, sizeof(Map)); + for(i = 0; i < nmap; i++){ + map[i].theirs = Zhash; + map[i].ours = ours[i]; + map[i].ref = refs[i]; + } + while(1){ + n = readpkt(c, buf, sizeof(buf)); + if(n == -1) + return -1; + if(n == 0) + break; + if(first && n > strlen(buf)) + parsecaps(buf + strlen(buf) + 1, &cs); + first = 0; + if(strncmp(buf, "ERR ", 4) == 0) + sysfatal("%s", buf + 4); + + if(getfields(buf, sp, nelem(sp), 1, " \t\r\n") != 2) + sysfatal("invalid ref line %.*s", utfnlen(buf, n), buf); + theirs = earealloc(theirs, ntheirs+1, sizeof(Hash)); + if(hparse(&theirs[ntheirs], sp[0]) == -1) + sysfatal("invalid hash %s", sp[0]); + if((idx = findkey(map, nmap, sp[1])) != -1) + map[idx].theirs = theirs[ntheirs]; + /* + * we only keep their ref if we can read the object to add it + * to our reachability; otherwise, discard it; we only care + * that we don't have it, so we can tell whether we need to + * bail out of pushing. + */ + if((o = readobject(theirs[ntheirs])) != nil){ + ntheirs++; + unref(o); + } + } + + if(writephase(c) == -1) + return -1; + send = 0; + if(force) + send=1; + for(i = 0; i < nmap; i++){ + m = &map[i]; + a = readobject(m->theirs); + if(hasheq(&m->ours, &Zhash)) + b = nil; + else + b = readobject(m->ours); + p = nil; + if(a != nil && b != nil) + p = ancestor(a, b); + if(!force && !hasheq(&m->theirs, &Zhash) && (a == nil || p != a)){ + fprint(2, "remote has diverged\n"); + werrstr("remote diverged"); + flushpkt(c); + return -1; + } + unref(a); + unref(b); + unref(p); + if(hasheq(&m->theirs, &m->ours)){ + print("uptodate %s\n", m->ref); + continue; + } + print("update %s %H %H\n", m->ref, m->theirs, m->ours); + n = snprint(buf, sizeof(buf), "%H %H %s", m->theirs, m->ours, m->ref); + + /* + * Workaround for github. + * + * Github will accept the pack but fail to update the references + * if we don't have capabilities advertised. Report-status seems + * harmless to add, so we add it. + * + * Github doesn't advertise any capabilities, so we can't check + * for compatibility. We just need to add it blindly. + */ + if(i == 0 && cs.report){ + buf[n++] = '\0'; + n += snprint(buf + n, sizeof(buf) - n, " report-status"); + } + if(writepkt(c, buf, n) == -1) + sysfatal("unable to send update pkt"); + send = 1; + } + flushpkt(c); + if(!send){ + fprint(2, "nothing to send\n"); + return 0; + } + + if(writepack(c->wfd, ours, nours, theirs, ntheirs, &h) == -1) + return -1; + if(!cs.report) + return 0; + + if(readphase(c) == -1) + return -1; + /* We asked for a status report, may as well use it. */ + while((n = readpkt(c, buf, sizeof(buf))) > 0){ + buf[n] = 0; + if(chattygit) + fprint(2, "done sending pack, status %s\n", buf); + nsp = getfields(buf, sp, nelem(sp), 1, " \t\n\r"); + if(nsp < 2) + continue; + if(nsp < 3) + sp[2] = ""; + /* + * Only report errors; successes will be reported by + * surrounding scripts. + */ + if(strcmp(sp[0], "unpack") == 0 && strcmp(sp[1], "ok") != 0) + fprint(2, "unpack %s\n", sp[1]); + else if(strcmp(sp[0], "ng") == 0) + fprint(2, "failed update: %s\n", sp[1]); + else + continue; + return -1; + } + return 0; +} + +void +usage(void) +{ + fprint(2, "usage: %s remote [reponame]\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char *br; + Conn c; + + ARGBEGIN{ + default: + usage(); + break; + case 'd': + chattygit++; + break; + case 'f': + force++; + break; + case 'r': + if(nremoved == nelem(removed)) + sysfatal("too many deleted branches"); + removed[nremoved++] = EARGF(usage()); + break; + case 'a': + sendall++; + break; + case 'b': + br = EARGF(usage()); + if(strncmp(br, "refs/heads/", strlen("refs/heads/")) == 0) + br = smprint("%s", br); + else if(strncmp(br, "heads/", strlen("heads/")) == 0) + br = smprint("refs/%s", br); + else + br = smprint("refs/heads/%s", br); + branch = erealloc(branch, (nbranch + 1)*sizeof(char*)); + branch[nbranch] = br; + nbranch++; + break; + }ARGEND; + + gitinit(); + if(argc != 1) + usage(); + if(gitconnect(&c, argv[0], "receive") == -1) + sysfatal("git connect: %s: %r", argv[0]); + if(sendpack(&c) == -1) + sysfatal("send failed: %r"); + closeconn(&c); + exits(nil); +} diff --git a/sys/src/cmd/git/serve.c b/sys/src/cmd/git/serve.c new file mode 100644 index 000000000..895c6f5e0 --- /dev/null +++ b/sys/src/cmd/git/serve.c @@ -0,0 +1,553 @@ +#include +#include +#include +#include + +#include "git.h" + +char *pathpfx = nil; +int allowwrite; + +int +fmtpkt(Conn *c, char *fmt, ...) +{ + char pkt[Pktmax]; + va_list ap; + int n; + + va_start(ap, fmt); + n = vsnprint(pkt, sizeof(pkt), fmt, ap); + n = writepkt(c, pkt, n); + va_end(ap); + return n; +} + +int +showrefs(Conn *c) +{ + int i, ret, nrefs; + Hash head, *refs; + char **names; + + ret = -1; + nrefs = 0; + refs = nil; + names = nil; + if(resolveref(&head, "HEAD") != -1) + if(fmtpkt(c, "%H HEAD\n", head) == -1) + goto error; + + if((nrefs = listrefs(&refs, &names)) == -1) + sysfatal("listrefs: %r"); + for(i = 0; i < nrefs; i++){ + if(strncmp(names[i], "heads/", strlen("heads/")) != 0) + continue; + if(fmtpkt(c, "%H refs/%s\n", refs[i], names[i]) == -1) + goto error; + } + if(flushpkt(c) == -1) + goto error; + ret = 0; +error: + for(i = 0; i < nrefs; i++) + free(names[i]); + free(names); + free(refs); + return ret; +} + +int +servnegotiate(Conn *c, Hash **head, int *nhead, Hash **tail, int *ntail) +{ + char pkt[Pktmax]; + int n, acked; + Object *o; + Hash h; + + if(showrefs(c) == -1) + return -1; + + *head = nil; + *tail = nil; + *nhead = 0; + *ntail = 0; + while(1){ + if((n = readpkt(c, pkt, sizeof(pkt))) == -1) + goto error; + if(n == 0) + break; + if(strncmp(pkt, "want ", 5) != 0){ + werrstr(" protocol garble %s", pkt); + goto error; + } + if(hparse(&h, &pkt[5]) == -1){ + werrstr(" garbled want"); + goto error; + } + if((o = readobject(h)) == nil){ + werrstr("requested nonexistent object"); + goto error; + } + unref(o); + *head = erealloc(*head, (*nhead + 1)*sizeof(Hash)); + (*head)[*nhead] = h; + *nhead += 1; + } + + acked = 0; + while(1){ + if((n = readpkt(c, pkt, sizeof(pkt))) == -1) + goto error; + if(strncmp(pkt, "done", 4) == 0) + break; + if(n == 0){ + if(!acked && fmtpkt(c, "NAK") == -1) + goto error; + } + if(strncmp(pkt, "have ", 5) != 0){ + werrstr(" protocol garble %s", pkt); + goto error; + } + if(hparse(&h, &pkt[5]) == -1){ + werrstr(" garbled have"); + goto error; + } + if((o = readobject(h)) == nil) + continue; + if(!acked){ + if(fmtpkt(c, "ACK %H", h) == -1) + goto error; + acked = 1; + } + unref(o); + *tail = erealloc(*tail, (*ntail + 1)*sizeof(Hash)); + (*tail)[*ntail] = h; + *ntail += 1; + } + if(!acked && fmtpkt(c, "NAK\n") == -1) + goto error; + return 0; +error: + fmtpkt(c, "ERR %r\n"); + free(*head); + free(*tail); + return -1; +} + +int +servpack(Conn *c) +{ + Hash *head, *tail, h; + int nhead, ntail; + + dprint(1, "negotiating pack\n"); + if(servnegotiate(c, &head, &nhead, &tail, &ntail) == -1) + sysfatal("negotiate: %r"); + dprint(1, "writing pack\n"); + if(writepack(c->wfd, head, nhead, tail, ntail, &h) == -1) + sysfatal("send: %r"); + return 0; +} + +int +validref(char *s) +{ + if(strncmp(s, "refs/", 5) != 0) + return 0; + for(; *s != '\0'; s++) + if(!isalnum(*s) && strchr("/-_.", *s) == nil) + return 0; + return 1; +} + +int +recvnegotiate(Conn *c, Hash **cur, Hash **upd, char ***ref, int *nupd) +{ + char pkt[Pktmax], *sp[4]; + Hash old, new; + int n, i; + + if(showrefs(c) == -1) + return -1; + *cur = nil; + *upd = nil; + *ref = nil; + *nupd = 0; + while(1){ + if((n = readpkt(c, pkt, sizeof(pkt))) == -1) + goto error; + if(n == 0) + break; + if(getfields(pkt, sp, nelem(sp), 1, " \t\n\r") != 3){ + fmtpkt(c, "ERR protocol garble %s\n", pkt); + goto error; + } + if(hparse(&old, sp[0]) == -1){ + fmtpkt(c, "ERR bad old hash %s\n", sp[0]); + goto error; + } + if(hparse(&new, sp[1]) == -1){ + fmtpkt(c, "ERR bad new hash %s\n", sp[1]); + goto error; + } + if(!validref(sp[2])){ + fmtpkt(c, "ERR invalid ref %s\n", sp[2]); + goto error; + } + *cur = erealloc(*cur, (*nupd + 1)*sizeof(Hash)); + *upd = erealloc(*upd, (*nupd + 1)*sizeof(Hash)); + *ref = erealloc(*ref, (*nupd + 1)*sizeof(Hash)); + (*cur)[*nupd] = old; + (*upd)[*nupd] = new; + (*ref)[*nupd] = estrdup(sp[2]); + *nupd += 1; + } + return 0; +error: + free(*cur); + free(*upd); + for(i = 0; i < *nupd; i++) + free((*ref)[i]); + free(*ref); + return -1; +} + +int +rename(char *pack, char *idx, Hash h) +{ + char name[128], path[196]; + Dir st; + + nulldir(&st); + st.name = name; + snprint(name, sizeof(name), "%H.pack", h); + snprint(path, sizeof(path), ".git/objects/pack/%s", name); + if(access(path, AEXIST) == 0) + fprint(2, "warning, pack %s already pushed\n", name); + else if(dirwstat(pack, &st) == -1) + return -1; + snprint(name, sizeof(name), "%H.idx", h); + snprint(path, sizeof(path), ".git/objects/pack/%s", name); + if(access(path, AEXIST) == 0) + fprint(2, "warning, pack %s already indexed\n", name); + else if(dirwstat(idx, &st) == -1) + return -1; + return 0; +} + +int +checkhash(int fd, vlong sz, Hash *hcomp) +{ + DigestState *st; + Hash hexpect; + char buf[Pktmax]; + vlong n, r; + int nr; + + if(sz < 28){ + werrstr("undersize packfile"); + return -1; + } + + st = nil; + n = 0; + if(seek(fd, 0, 0) == -1) + sysfatal("packfile seek: %r"); + while(n != sz - 20){ + nr = sizeof(buf); + if(sz - n - 20 < sizeof(buf)) + nr = sz - n - 20; + r = readn(fd, buf, nr); + if(r != nr){ + werrstr("short read"); + return -1; + } + st = sha1((uchar*)buf, nr, nil, st); + n += r; + } + sha1(nil, 0, hcomp->h, st); + if(readn(fd, hexpect.h, sizeof(hexpect.h)) != sizeof(hexpect.h)) + sysfatal("truncated packfile"); + if(!hasheq(hcomp, &hexpect)){ + werrstr("bad hash: %H != %H", *hcomp, hexpect); + return -1; + } + return 0; +} + +int +mkdir(char *dir) +{ + char buf[ERRMAX]; + int f; + + if(access(dir, AEXIST) == 0) + return 0; + if((f = create(dir, OREAD, DMDIR | 0755)) == -1){ + rerrstr(buf, sizeof(buf)); + if(strstr(buf, "exist") == nil) + return -1; + } + close(f); + return 0; +} + +int +updatepack(Conn *c) +{ + char buf[Pktmax], packtmp[128], idxtmp[128], ebuf[ERRMAX]; + int n, pfd, packsz; + Hash h; + + /* make sure the needed dirs exist */ + if(mkdir(".git/objects") == -1) + return -1; + if(mkdir(".git/objects/pack") == -1) + return -1; + if(mkdir(".git/refs") == -1) + return -1; + if(mkdir(".git/refs/heads") == -1) + return -1; + snprint(packtmp, sizeof(packtmp), ".git/objects/pack/recv-%d.pack.tmp", getpid()); + snprint(idxtmp, sizeof(idxtmp), ".git/objects/pack/recv-%d.idx.tmp", getpid()); + if((pfd = create(packtmp, ORDWR, 0644)) == -1) + return -1; + packsz = 0; + while(1){ + n = read(c->rfd, buf, sizeof(buf)); + if(n == 0) + break; + if(n == -1){ + rerrstr(ebuf, sizeof(ebuf)); + if(strstr(ebuf, "hungup") == nil) + return -1; + break; + } + if(write(pfd, buf, n) != n) + return -1; + packsz += n; + } + if(checkhash(pfd, packsz, &h) == -1){ + dprint(1, "hash mismatch\n"); + goto error1; + } + if(indexpack(packtmp, idxtmp, h) == -1){ + dprint(1, "indexing failed: %r\n"); + goto error1; + } + if(rename(packtmp, idxtmp, h) == -1){ + dprint(1, "rename failed: %r\n"); + goto error2; + } + return 0; + +error2: remove(idxtmp); +error1: remove(packtmp); + return -1; +} + +int +lockrepo(void) +{ + int fd, i; + + for(i = 0; i < 10; i++) { + if((fd = create(".git/_lock", ORCLOSE|ORDWR|OTRUNC|OEXCL, 0644))!= -1) + return fd; + sleep(250); + } + return -1; +} + +int +updaterefs(Conn *c, Hash *cur, Hash *upd, char **ref, int nupd) +{ + char refpath[512], buf[128]; + int i, newidx, hadref, fd, ret, lockfd; + vlong newtm; + Object *o; + Hash h; + + ret = -1; + hadref = 0; + newidx = -1; + /* + * Date of Magna Carta. + * Wrong because it was computed using + * the proleptic gregorian calendar. + */ + newtm = -23811206400; + if((lockfd = lockrepo()) == -1){ + snprint(buf, sizeof(buf), "repo locked\n"); + return -1; + } + for(i = 0; i < nupd; i++){ + if(resolveref(&h, ref[i]) == 0){ + hadref = 1; + if(!hasheq(&h, &cur[i])){ + snprint(buf, sizeof(buf), "old ref changed: %s", ref[i]); + goto error; + } + } + if(snprint(refpath, sizeof(refpath), ".git/%s", ref[i]) == sizeof(refpath)){ + snprint(buf, sizeof(buf), "ref path too long: %s", ref[i]); + goto error; + } + if(hasheq(&upd[i], &Zhash)){ + remove(refpath); + continue; + } + if((o = readobject(upd[i])) == nil){ + snprint(buf, sizeof(buf), "update to nonexistent hash %H", upd[i]); + goto error; + } + if(o->type != GCommit){ + snprint(buf, sizeof(buf), "not commit: %H", upd[i]); + goto error; + } + if(o->commit->mtime > newtm){ + newtm = o->commit->mtime; + newidx = i; + } + unref(o); + if((fd = create(refpath, OWRITE|OTRUNC, 0644)) == -1){ + snprint(buf, sizeof(buf), "open ref: %r"); + goto error; + } + if(fprint(fd, "%H", upd[i]) == -1){ + snprint(buf, sizeof(buf), "upate ref: %r"); + close(fd); + goto error; + } + close(fd); + } + /* + * Heuristic: + * If there are no valid refs, and HEAD is invalid, then + * pick the ref with the newest commits as the default + * branch. + * + * Several people have been caught out by pushing to + * a repo where HEAD named differently from what got + * pushed, and this is going to be more of a footgun + * when 'master', 'main', and 'front' are all in active + * use. This should make us pick a useful default in + * those cases, instead of silently failing. + */ + if(resolveref(&h, "HEAD") == -1 && hadref == 0 && newidx != -1){ + if((fd = create(".git/HEAD", OWRITE|OTRUNC, 0644)) == -1){ + snprint(buf, sizeof(buf), "open HEAD: %r"); + goto error; + } + if(fprint(fd, "ref: %s", ref[0]) == -1){ + snprint(buf, sizeof(buf), "write HEAD ref: %r"); + goto error; + } + close(fd); + } + ret = 0; +error: + fmtpkt(c, "ERR %s", buf); + close(lockfd); + werrstr(buf); + return ret; +} + +int +recvpack(Conn *c) +{ + Hash *cur, *upd; + char **ref; + int nupd; + + if(recvnegotiate(c, &cur, &upd, &ref, &nupd) == -1) + sysfatal("negotiate refs: %r"); + if(nupd != 0 && updatepack(c) == -1) + sysfatal("update pack: %r"); + if(nupd != 0 && updaterefs(c, cur, upd, ref, nupd) == -1) + sysfatal("update refs: %r"); + return 0; +} + +char* +parsecmd(char *buf, char *cmd, int ncmd) +{ + int i; + char *p; + + for(p = buf, i = 0; *p && i < ncmd - 1; i++, p++){ + if(*p == ' ' || *p == '\t'){ + cmd[i] = 0; + break; + } + cmd[i] = *p; + } + while(*p == ' ' || *p == '\t') + p++; + return p; +} + +void +usage(void) +{ + fprint(2, "usage: %s [-dw] [-r rel]\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char *repo, cmd[32], buf[512]; + Conn c; + + ARGBEGIN{ + case 'd': + chattygit++; + break; + case 'r': + pathpfx = EARGF(usage()); + if(*pathpfx != '/') + sysfatal("path prefix must begin with '/'"); + break; + case 'w': + allowwrite++; + break; + default: + usage(); + break; + }ARGEND; + + gitinit(); + interactive = 0; + if(rfork(RFNAMEG) == -1) + sysfatal("rfork: %r"); + if(pathpfx != nil){ + if(bind(pathpfx, "/", MREPL) == -1) + sysfatal("bind: %r"); + } + if(rfork(RFNOMNT) == -1) + sysfatal("rfork: %r"); + + initconn(&c, 0, 1); + if(readpkt(&c, buf, sizeof(buf)) == -1) + sysfatal("readpkt: %r"); + repo = parsecmd(buf, cmd, sizeof(cmd)); + cleanname(repo); + if(strncmp(repo, "../", 3) == 0) + sysfatal("invalid path %s\n", repo); + if(bind(repo, "/", MREPL) == -1){ + fmtpkt(&c, "ERR no repo %r\n"); + sysfatal("enter %s: %r", repo); + } + if(chdir("/") == -1) + sysfatal("chdir: %r"); + if(access(".git", AREAD) == -1) + sysfatal("no git repository"); + if(strcmp(cmd, "git-receive-pack") == 0 && allowwrite) + recvpack(&c); + else if(strcmp(cmd, "git-upload-pack") == 0) + servpack(&c); + else + sysfatal("unsupported command '%s'", cmd); + exits(nil); +} diff --git a/sys/src/cmd/git/util.c b/sys/src/cmd/git/util.c new file mode 100644 index 000000000..1d2398a7e --- /dev/null +++ b/sys/src/cmd/git/util.c @@ -0,0 +1,393 @@ +#include +#include +#include + +#include "git.h" + +Reprog *authorpat; +Hash Zhash; + +int chattygit; +int interactive = 1; + +Object* +emptydir(void) +{ + static Object *e; + + if(e != nil) + return ref(e); + e = emalloc(sizeof(Object)); + e->hash = Zhash; + e->type = GTree; + e->tree = emalloc(sizeof(Tinfo)); + e->tree->ent = nil; + e->tree->nent = 0; + e->flag |= Cloaded|Cparsed; + e->off = -1; + ref(e); + cache(e); + return e; +} + +int +hasheq(Hash *a, Hash *b) +{ + return memcmp(a->h, b->h, sizeof(a->h)) == 0; +} + +static int +charval(int c, int *err) +{ + if(c >= '0' && c <= '9') + return c - '0'; + if(c >= 'a' && c <= 'f') + return c - 'a' + 10; + if(c >= 'A' && c <= 'F') + return c - 'A' + 10; + *err = 1; + return -1; +} + +void * +emalloc(ulong n) +{ + void *v; + + v = mallocz(n, 1); + if(v == nil) + sysfatal("malloc: %r"); + setmalloctag(v, getcallerpc(&n)); + return v; +} + +void * +eamalloc(ulong n, ulong sz) +{ + uvlong na; + void *v; + + na = (uvlong)n*(uvlong)sz; + if(na >= (1ULL<<30)) + sysfatal("alloc: overflow"); + v = mallocz(na, 1); + if(v == nil) + sysfatal("malloc: %r"); + setmalloctag(v, getcallerpc(&n)); + return v; +} + +void * +erealloc(void *p, ulong n) +{ + void *v; + + v = realloc(p, n); + if(v == nil) + sysfatal("realloc: %r"); + setmalloctag(v, getcallerpc(&p)); + return v; +} + +void * +earealloc(void *p, ulong n, ulong sz) +{ + uvlong na; + void *v; + + na = (uvlong)n*(uvlong)sz; + if(na >= (1ULL<<30)) + sysfatal("alloc: overflow"); + v = realloc(p, na); + if(v == nil) + sysfatal("realloc: %r"); + setmalloctag(v, getcallerpc(&p)); + return v; +} + +char* +estrdup(char *s) +{ + s = strdup(s); + if(s == nil) + sysfatal("strdup: %r"); + setmalloctag(s, getcallerpc(&s)); + return s; +} + +int +Hfmt(Fmt *fmt) +{ + Hash h; + int i, n, l; + char c0, c1; + + l = 0; + h = va_arg(fmt->args, Hash); + for(i = 0; i < sizeof h.h; i++){ + n = (h.h[i] >> 4) & 0xf; + c0 = (n >= 10) ? n-10 + 'a' : n + '0'; + n = h.h[i] & 0xf; + c1 = (n >= 10) ? n-10 + 'a' : n + '0'; + l += fmtprint(fmt, "%c%c", c0, c1); + } + return l; +} + +int +Tfmt(Fmt *fmt) +{ + int t; + int l; + + t = va_arg(fmt->args, int); + switch(t){ + case GNone: l = fmtprint(fmt, "none"); break; + case GCommit: l = fmtprint(fmt, "commit"); break; + case GTree: l = fmtprint(fmt, "tree"); break; + case GBlob: l = fmtprint(fmt, "blob"); break; + case GTag: l = fmtprint(fmt, "tag"); break; + case GOdelta: l = fmtprint(fmt, "odelta"); break; + case GRdelta: l = fmtprint(fmt, "gdelta"); break; + default: l = fmtprint(fmt, "?%d?", t); break; + } + return l; +} + +int +Ofmt(Fmt *fmt) +{ + Object *o; + int l; + + o = va_arg(fmt->args, Object *); + print("== %H (%T) ==\n", o->hash, o->type); + switch(o->type){ + case GTree: + l = fmtprint(fmt, "tree\n"); + break; + case GBlob: + l = fmtprint(fmt, "blob %s\n", o->data); + break; + case GCommit: + l = fmtprint(fmt, "commit\n"); + break; + case GTag: + l = fmtprint(fmt, "tag\n"); + break; + default: + l = fmtprint(fmt, "invalid: %d\n", o->type); + break; + } + return l; +} + +int +Qfmt(Fmt *fmt) +{ + Qid q; + + q = va_arg(fmt->args, Qid); + return fmtprint(fmt, "Qid{path=0x%llx(dir:%d,obj:%lld), vers=%ld, type=%d}", + q.path, QDIR(&q), (q.path >> 8), q.vers, q.type); +} + +void +gitinit(void) +{ + fmtinstall('H', Hfmt); + fmtinstall('T', Tfmt); + fmtinstall('O', Ofmt); + fmtinstall('Q', Qfmt); + inflateinit(); + deflateinit(); + authorpat = regcomp("[\t ]*(.*)[\t ]+([0-9]+)[\t ]+([\\-+]?[0-9]+)"); + osinit(&objcache); +} + +int +hparse(Hash *h, char *b) +{ + int i, err; + + err = 0; + for(i = 0; i < sizeof(h->h); i++){ + err = 0; + h->h[i] = 0; + h->h[i] |= ((charval(b[2*i], &err) & 0xf) << 4); + h->h[i] |= ((charval(b[2*i+1], &err)& 0xf) << 0); + if(err){ + werrstr("invalid hash"); + return -1; + } + } + return 0; +} + +int +slurpdir(char *p, Dir **d) +{ + int r, f; + + if((f = open(p, OREAD)) == -1) + return -1; + r = dirreadall(f, d); + close(f); + return r; +} + +int +hassuffix(char *base, char *suf) +{ + int nb, ns; + + nb = strlen(base); + ns = strlen(suf); + if(ns <= nb && strcmp(base + (nb - ns), suf) == 0) + return 1; + return 0; +} + +int +swapsuffix(char *dst, int dstsz, char *base, char *oldsuf, char *suf) +{ + int bl, ol, sl, l; + + bl = strlen(base); + ol = strlen(oldsuf); + sl = strlen(suf); + l = bl + sl - ol; + if(l + 1 > dstsz || ol > bl) + return -1; + memmove(dst, base, bl - ol); + memmove(dst + bl - ol, suf, sl); + dst[l] = 0; + return l; +} + +char * +strip(char *s) +{ + char *e; + + while(isspace(*s)) + s++; + e = s + strlen(s); + while(e > s && isspace(*--e)) + *e = 0; + return s; +} + +void +_dprint(char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprint(2, fmt, ap); + va_end(ap); +} + +/* Finds the directory containing the git repo. */ +int +findrepo(char *buf, int nbuf) +{ + char *p, *suff; + + suff = "/.git/HEAD"; + if(getwd(buf, nbuf - strlen(suff) - 1) == nil) + return -1; + + for(p = buf + strlen(buf); p != nil; p = strrchr(buf, '/')){ + strcpy(p, suff); + if(access(buf, AEXIST) == 0){ + p[p == buf] = '\0'; + return 0; + } + *p = '\0'; + } + werrstr("not a git repository"); + return -1; +} + +int +showprogress(int x, int pct) +{ + if(!interactive) + return 0; + if(x > pct){ + pct = x; + fprint(2, "\b\b\b\b%3d%%", pct); + } + return pct; +} + +void +qinit(Objq *q) +{ + memset(q, 0, sizeof(Objq)); + q->nheap = 0; + q->heapsz = 8; + q->heap = eamalloc(q->heapsz, sizeof(Qelt)); +} + +void +qclear(Objq *q) +{ + free(q->heap); +} + +void +qput(Objq *q, Object *o, int color) +{ + Qelt t; + int i; + + if(q->nheap == q->heapsz){ + q->heapsz *= 2; + q->heap = earealloc(q->heap, q->heapsz, sizeof(Qelt)); + } + q->heap[q->nheap].o = o; + q->heap[q->nheap].color = color; + q->heap[q->nheap].ctime = o->commit->ctime; + for(i = q->nheap; i > 0; i = (i-1)/2){ + if(q->heap[i].ctime < q->heap[(i-1)/2].ctime) + break; + t = q->heap[i]; + q->heap[i] = q->heap[(i-1)/2]; + q->heap[(i-1)/2] = t; + } + q->nheap++; +} + +int +qpop(Objq *q, Qelt *e) +{ + int i, l, r, m; + Qelt t; + + if(q->nheap == 0) + return 0; + *e = q->heap[0]; + if(--q->nheap == 0) + return 1; + + i = 0; + q->heap[0] = q->heap[q->nheap]; + while(1){ + m = i; + l = 2*i+1; + r = 2*i+2; + if(l < q->nheap && q->heap[m].ctime < q->heap[l].ctime) + m = l; + if(r < q->nheap && q->heap[m].ctime < q->heap[r].ctime) + m = r; + if(m == i) + break; + t = q->heap[m]; + q->heap[m] = q->heap[i]; + q->heap[i] = t; + i = m; + } + return 1; +} diff --git a/sys/src/cmd/git/walk.c b/sys/src/cmd/git/walk.c new file mode 100644 index 000000000..50a227e21 --- /dev/null +++ b/sys/src/cmd/git/walk.c @@ -0,0 +1,331 @@ +#include +#include +#include "git.h" + +#define NCACHE 4096 +#define TDIR ".git/index9/tracked" +#define RDIR ".git/index9/removed" +#define HDIR ".git/fs/HEAD/tree" +typedef struct Cache Cache; +typedef struct Wres Wres; +struct Cache { + Dir* cache; + int n; + int max; +}; + +struct Wres { + char **path; + int npath; + int pathsz; +}; + +enum { + Rflg = 1 << 0, + Mflg = 1 << 1, + Aflg = 1 << 2, + Tflg = 1 << 3, +}; + +Cache seencache[NCACHE]; +int quiet; +int printflg; +char *rstr = "R "; +char *tstr = "T "; +char *mstr = "M "; +char *astr = "A "; + +int +seen(Dir *dir) +{ + Dir *dp; + int i; + Cache *c; + + c = &seencache[dir->qid.path&(NCACHE-1)]; + dp = c->cache; + for(i=0; in; i++, dp++) + if(dir->qid.path == dp->qid.path && + dir->type == dp->type && + dir->dev == dp->dev) + return 1; + if(c->n == c->max){ + if (c->max == 0) + c->max = 8; + else + c->max += c->max/2; + c->cache = realloc(c->cache, c->max*sizeof(Dir)); + if(c->cache == nil) + sysfatal("realloc: %r"); + } + c->cache[c->n++] = *dir; + return 0; +} + +void +grow(Wres *r) +{ + if(r->npath == r->pathsz){ + r->pathsz = 2*r->pathsz + 1; + r->path = erealloc(r->path, r->pathsz * sizeof(char*)); + } +} + +int +readpaths(Wres *r, char *pfx, char *dir) +{ + char *f, *sub, *full, *sep; + Dir *d; + int fd, ret, i, n; + + d = nil; + ret = -1; + sep = ""; + if(dir[0] != 0) + sep = "/"; + if((full = smprint("%s/%s", pfx, dir)) == nil) + sysfatal("smprint: %r"); + if((fd = open(full, OREAD)) < 0) + goto error; + while((n = dirread(fd, &d)) > 0){ + for(i = 0; i < n; i++){ + if(seen(&d[i])) + continue; + if(d[i].qid.type & QTDIR){ + if((sub = smprint("%s%s%s", dir, sep, d[i].name)) == nil) + sysfatal("smprint: %r"); + if(readpaths(r, pfx, sub) == -1){ + free(sub); + goto error; + } + free(sub); + }else{ + grow(r); + if((f = smprint("%s%s%s", dir, sep, d[i].name)) == nil) + sysfatal("smprint: %r"); + r->path[r->npath++] = f; + } + } + free(d); + } + ret = r->npath; +error: + close(fd); + free(full); + return ret; +} + +int +cmp(void *pa, void *pb) +{ + return strcmp(*(char **)pa, *(char **)pb); +} + +void +dedup(Wres *r) +{ + int i, o; + + if(r->npath <= 1) + return; + o = 0; + qsort(r->path, r->npath, sizeof(r->path[0]), cmp); + for(i = 1; i < r->npath; i++) + if(strcmp(r->path[o], r->path[i]) != 0) + r->path[++o] = r->path[i]; + r->npath = o + 1; +} + +int +sameqid(Dir *d, char *qf) +{ + char indexqid[64], fileqid[64], *p; + int fd, n; + + if(!d) + return 0; + if((fd = open(qf, OREAD)) == -1) + return 0; + if((n = readn(fd, indexqid, sizeof(indexqid) - 1)) == -1) + return 0; + indexqid[n] = 0; + close(fd); + if((p = strpbrk(indexqid, " \t\n\r")) != nil) + *p = 0; + + snprint(fileqid, sizeof(fileqid), "%ullx.%uld.%.2uhhx", + d->qid.path, d->qid.vers, d->qid.type); + + if(strcmp(indexqid, fileqid) == 0) + return 1; + return 0; +} + +void +writeqid(Dir *d, char *qf) +{ + int fd; + + if((fd = create(qf, OWRITE, 0666)) == -1) + return; + fprint(fd, "%ullx.%uld.%.2uhhx\n", + d->qid.path, d->qid.vers, d->qid.type); + close(fd); +} + +int +samedata(char *pa, char *pb) +{ + char ba[32*1024], bb[32*1024]; + int fa, fb, na, nb, same; + + same = 0; + fa = open(pa, OREAD); + fb = open(pb, OREAD); + if(fa == -1 || fb == -1){ + goto mismatch; + } + while(1){ + if((na = readn(fa, ba, sizeof(ba))) == -1) + goto mismatch; + if((nb = readn(fb, bb, sizeof(bb))) == -1) + goto mismatch; + if(na != nb) + goto mismatch; + if(na == 0) + break; + if(memcmp(ba, bb, na) != 0) + goto mismatch; + } + same = 1; +mismatch: + if(fa != -1) + close(fa); + if(fb != -1) + close(fb); + return same; +} + +void +usage(void) +{ + fprint(2, "usage: %s [-qbc] [-f filt] [paths...]\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char *rpath, *tpath, *bpath, buf[8], repo[512]; + char *p, *e; + int i, dirty; + Wres r; + Dir *d; + + ARGBEGIN{ + case 'q': + quiet++; + break; + case 'c': + rstr = ""; + tstr = ""; + mstr = ""; + astr = ""; + break; + case 'f': + for(p = EARGF(usage()); *p; p++) + switch(*p){ + case 'T': printflg |= Tflg; break; + case 'A': printflg |= Aflg; break; + case 'M': printflg |= Mflg; break; + case 'R': printflg |= Rflg; break; + default: usage(); break; + } + break; + default: + usage(); + }ARGEND + + if(findrepo(repo, sizeof(repo)) == -1) + sysfatal("find root: %r"); + if(chdir(repo) == -1) + sysfatal("chdir: %r"); + if(access(".git/fs/ctl", AEXIST) != 0) + sysfatal("no running git/fs"); + dirty = 0; + memset(&r, 0, sizeof(r)); + if(printflg == 0) + printflg = Tflg | Aflg | Mflg | Rflg; + if(argc == 0){ + if(access(TDIR, AEXIST) == 0 && readpaths(&r, TDIR, "") == -1) + sysfatal("read tracked: %r"); + if(access(RDIR, AEXIST) == 0 && readpaths(&r, RDIR, "") == -1) + sysfatal("read removed: %r"); + }else{ + for(i = 0; i < argc; i++){ + tpath = smprint(TDIR"/%s", argv[i]); + rpath = smprint(RDIR"/%s", argv[i]); + if((d = dirstat(tpath)) == nil && (d = dirstat(rpath)) == nil) + goto nextarg; + if(d->mode & DMDIR){ + readpaths(&r, TDIR, argv[i]); + readpaths(&r, RDIR, argv[i]); + }else{ + grow(&r); + r.path[r.npath++] = estrdup(argv[i]); + } +nextarg: + free(tpath); + free(rpath); + free(d); + } + } + dedup(&r); + + for(i = 0; i < r.npath; i++){ + p = r.path[i]; + d = dirstat(p); + if(d && d->mode & DMDIR) + goto next; + rpath = smprint(RDIR"/%s", p); + tpath = smprint(TDIR"/%s", p); + bpath = smprint(HDIR"/%s", p); + /* Fast path: we don't want to force access to the rpath. */ + if(d && sameqid(d, tpath)) { + if(!quiet && (printflg & Tflg)) + print("%s%s\n", tstr, p); + }else{ + if(d == nil || access(rpath, AEXIST) == 0){ + dirty |= Rflg; + if(!quiet && (printflg & Rflg)) + print("%s%s\n", rstr, p); + }else if(access(bpath, AEXIST) == -1) { + dirty |= Aflg; + if(!quiet && (printflg & Aflg)) + print("%s%s\n", astr, p); + }else if(samedata(p, bpath)){ + if(!quiet && (printflg & Tflg)) + print("%s%s\n", tstr, p); + writeqid(d, tpath); + }else{ + dirty |= Mflg; + if(!quiet && (printflg & Mflg)) + print("%s%s\n", mstr, p); + } + } + free(rpath); + free(tpath); + free(bpath); +next: + free(d); + } + if(!dirty) + exits(nil); + + p = buf; + e = buf + sizeof(buf); + for(i = 0; (1 << i) != Tflg; i++) + if(dirty & (1 << i)) + p = seprint(p, e, "%c", "DMAT"[i]); + exits(buf); +}