git: got git?

Add a snapshot of git9 to 9front.
This commit is contained in:
Ori Bernstein 2021-05-16 18:49:45 -07:00
parent 013b2cad19
commit 1ee1bfaa8c
36 changed files with 9308 additions and 0 deletions

643
sys/man/1/git Normal file
View file

@ -0,0 +1,643 @@
.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/rm
.I path...
.PP
.B git/branch
[
.B -adns
]
[
.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/revert
[
.B -c
.I commit
]
.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 -f
]
[
.B -q
]
[
.B -a
]
[
.B -u
.I upstream
]
.PP
.B git/push
[
.B -a
]
[
.B -u
.I upstream
]
[
.B -b
.I branch
]
[
.B -r
.I branch
]
.PP
.B git/serve
[
.B -w
]
[
.B -r
.I path
]
.PP
.B git/query
[
.B -pcr
]
.I query
.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 /mnt/git.
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 changed 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 -u upstream
option, the changes are pulled from
.I upstream
instead of the configured origin.
.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 /mnt/git.
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
.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
Removed from either working directory tracking list.
.TP
A
Added, does not yet exist in a 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 global configuration for git.
The contents of this file are used as fallbacks for the per-repository config.
.SH SEE ALSO
.IR hg (1)
.IR replica (1)
.IR patch (1)
.IR gitfs (4)
.IR diff3
.SH BUGS
.PP
Repositories with submodules are effectively read-only.
.PP
There are a some of missing commands, features, and tools, such as git/rebase
.PP
git/compat only works within a git repository.

112
sys/man/4/gitfs Normal file
View file

@ -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 /mnt/git.
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 <email@domain.here>
.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 /mnt/git/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.

39
sys/src/cmd/git/add Executable file
View file

@ -0,0 +1,39 @@
#!/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
if(~ $add tracked)
files=`$nl{walk -f $gitrel/$*}
if not
files=`$nl{cd .git/index9/tracked/ && walk -f $gitrel/$*}
for(f in $files){
if(! ~ `{cleanname $f} .git/*){
addpath=.git/index9/$add/$f
delpath=.git/index9/$del/$f
mkdir -p `{basename -d $addpath}
mkdir -p `{basename -d $delpath}
# We don't want a matching qid, so that
# git/walk doesn't think this came from
# a checkout.
if(! test -e $addpath)
if(~ $add 'tracked' || test -e /mnt/git/HEAD/tree/$f)
touch $addpath
rm -f $delpath
}
}
exit ''

109
sys/src/cmd/git/branch Executable file
View file

@ -0,0 +1,109 @@
#!/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}' < /mnt/git/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(test -e .git/$new)
base=`{git/query $new}
if not
base=`{git/query HEAD}
modified=`$nl{git/query -c HEAD $base | grep '^[^-]' | subst '^..'}
deleted=`$nl{git/query -c HEAD $base | grep '^-' | subst '^..'}
if(! ~ $#modified 0 || ! ~ $#deleted 0 && ~ $#merge 0){
git/walk -fRMA $modified $deleted ||
die 'uncommited changes would be clobbered'
}
if(~ $delete 1){
rm -f .git/$new
echo 'deleted branch' $new
exit
}
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
}
commit=`{git/query $base} || die 'branch does not exist:' $base
if(~ $new */*)
mkdir -p .git/`{basename -d $new}
echo $commit > .git/$new
if(! ~ $#stay 0)
exit
basedir=`{git/query -p $base}
dirtypaths=()
cleanpaths=($modified $deleted)
if(! ~ $#modified 0 || ! ~ $#deleted 0)
dirtypaths=`$nl{git/walk -cfRMA $modified $deleted}
if(! ~ $#dirtypaths 0){
x=$nl^$cleanpaths
y=$nl^$dirtypaths
cleanpaths=`$nl{echo $"x$nl$"y | sort | uniq -u}
}
for(m in $cleanpaths){
d=`{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=`{test -f $m && echo file || echo dir}
b=`{test -f $basedir/tree/$m && echo file || echo dir}
if(! ~ $a $b){
rm -rf $m
rm -rf .git/index9/tracked/$m
}
if(test -f $basedir/tree/$m){
cp $basedir/tree/$m $m
walk -eq $m > .git/index9/tracked/$m
}
}
for(ours in $dirtypaths){
common=/mnt/git/object/$orig/tree/$ours
theirs=/mnt/git/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
exit ''

115
sys/src/cmd/git/clone Executable file
View file

@ -0,0 +1,115 @@
#!/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 | subst -g '/*$'}
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/objects/pack/
mkdir -p $local/.git/refs/heads/
cd $local
>>.git/config {
echo '[remote "origin"]'
echo ' url='$remote
}
{git/fetch $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=/mnt/git/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 | subst '^'$tree'/*'}){
if(! ~ $#f 0){
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 ''

150
sys/src/cmd/git/commit Executable file
View file

@ -0,0 +1,150 @@
#!/bin/rc -e
rfork ne
. /sys/lib/git/common.rc
fn whoami{
name=`{git/conf user.name}
email=`{git/conf user.email}
if(test -f /adm/keys.who){
if(~ $name '')
name=`{awk -F'|' '$1=="'$user'" {x=$3} END{print x}' </adm/keys.who}
if(~ $email '')
email=`{awk -F'|' '$1=="'$user'" {x=$5} END{print x}' </adm/keys.who}
}
if(~ $name '')
name=glenda
if(~ $email '')
email=glenda@9front.local
}
fn findbranch{
branch=`{git/branch}
if(test -e /mnt/git/branch/$branch/tree){
refpath=.git/refs/$branch
initial=false
}
if not if(test -e /mnt/git/object/$branch/tree){
refpath=.git/HEAD
initial=false
}
if not if(! test -e /mnt/git/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 -g '^' '# '
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 /mnt/git/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{
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 /mnt/git/HEAD/hash}
cat /mnt/git/HEAD/msg >$msgfile.tmp
}
files=()
if(! ~ $#* 0)
files=`$nl{git/walk -c `$nl{cleanname $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 ''

158
sys/src/cmd/git/compat Executable file
View file

@ -0,0 +1,158 @@
#!/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_version{
echo git version 2.2.0
}
fn usage{
echo 'git <command> <args>' >[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
cmd_$1 $*(2-)
exit ''

97
sys/src/cmd/git/conf.c Normal file
View file

@ -0,0 +1,97 @@
#include <u.h>
#include <libc.h>
#include <ctype.h>
#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);
}
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);
}

219
sys/src/cmd/git/delta.c Normal file
View file

@ -0,0 +1,219 @@
#include <u.h>
#include <libc.h>
#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;
}

37
sys/src/cmd/git/diff Executable file
View file

@ -0,0 +1,37 @@
#!/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 $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 '^..'
}
for(f in `$nl{lsdirty | sort | uniq}){
orig=$branch/tree/$f
if(! test -f $orig)
orig=/dev/null
if(! test -f $f)
f=/dev/null
diff -u $orig $f
}
exit ''

89
sys/src/cmd/git/export Executable file
View file

@ -0,0 +1,89 @@
#!/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 $c
echo From: `{cat $cp/author}
echo Date: `{date -um `{mtime $cp/author | awk '{print $1}'}}
<$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 ''

316
sys/src/cmd/git/fetch.c Normal file
View file

@ -0,0 +1,316 @@
#include <u.h>
#include <libc.h>
#include "git.h"
char *fetchbranch;
char *upstream = "origin";
char *packtmp = ".git/objects/pack/fetch.tmp";
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 | 0755);
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);
}
}
}
}
int
fetchpack(Conn *c, int pfd, char *packtmp)
{
char buf[Pktmax], idxtmp[256], *sp[3];
Hash h, *have, *want;
int nref, refsz, first;
int i, n, req;
vlong packsz;
Object *o;
nref = 0;
refsz = 16;
first = 1;
have = eamalloc(refsz, sizeof(have[0]));
want = eamalloc(refsz, sizeof(want[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 = erealloc(have, refsz * sizeof(have[0]));
want = erealloc(want, refsz * sizeof(want[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]));
print("remote %s %H local %H\n", sp[1], want[nref], have[nref]);
nref++;
}
if(listonly){
flushpkt(c);
return 0;
}
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);
for(i = 0; i < nref; i++){
if(hasheq(&have[i], &Zhash))
continue;
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]);
}
if(!req)
flushpkt(c);
n = snprint(buf, sizeof(buf), "done\n");
if(writepkt(c, buf, n) == -1)
sysfatal("write: %r");
if(!req)
return 0;
if(readphase(c) == -1)
sysfatal("read: %r");
if((n = readpkt(c, buf, sizeof(buf))) == -1)
sysfatal("read: %r");
buf[n] = 0;
fprint(2, "fetching...\n");
packsz = 0;
while(1){
n = readn(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)
sysfatal("packfile seek: %r");
if(checkhash(pfd, packsz, &h) == -1)
sysfatal("corrupt packfile: %r");
close(pfd);
n = strlen(packtmp) - strlen(".tmp");
memcpy(idxtmp, packtmp, n);
memcpy(idxtmp + n, ".idx", strlen(".idx") + 1);
if(indexpack(packtmp, idxtmp, h) == -1)
sysfatal("could not index fetched pack: %r");
if(rename(packtmp, idxtmp, h) == -1)
sysfatal("could not rename indexed pack: %r");
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)
{
int pfd;
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(mkoutpath(packtmp) == -1)
sysfatal("could not create %s: %r", packtmp);
if((pfd = create(packtmp, ORDWR, 0644)) == -1)
sysfatal("could not create %s: %r", packtmp);
if(gitconnect(&c, argv[0], "upload") == -1)
sysfatal("could not dial %s: %r", argv[0]);
if(fetchpack(&c, pfd, packtmp) == -1)
sysfatal("fetch failed: %r");
closeconn(&c);
exits(nil);
}

853
sys/src/cmd/git/fs.c Normal file
View file

@ -0,0 +1,853 @@
#include <u.h>
#include <libc.h>
#include <ctype.h>
#include <fcall.h>
#include <thread.h>
#include <9p.h>
#include "git.h"
enum {
Qroot,
Qhead,
Qbranch,
Qcommit,
Qcommitmsg,
Qcommitparent,
Qcommittree,
Qcommitdata,
Qcommithash,
Qcommitauthor,
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 Egreg "wat";
#define Ebadobj "invalid object";
char gitdir[512];
char *username;
char *mtpt = "/mnt/git";
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 <c->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(username);
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(username);
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(username);
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->mode |= (o->type == GTree) ? 0755 : 0644;
d->atime = c->mtime;
d->mtime = c->mtime;
d->uid = estrdup9p(username);
d->gid = estrdup9p(username);
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(username);
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, Qcommittree);
break;
case 1:
d->name = estrdup9p("parent");
d->qid.path = qpath(c, i, o->id, Qcommitparent);
break;
case 2:
d->name = estrdup9p("msg");
d->qid.path = qpath(c, i, o->id, Qcommitmsg);
break;
case 3:
d->name = estrdup9p("hash");
d->qid.path = qpath(c, i, o->id, Qcommithash);
break;
case 4:
d->name = estrdup9p("author");
d->qid.path = qpath(c, i, o->id, Qcommitauthor);
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;
int i, n;
n = o->commit->nparent * (40 + 2);
buf = emalloc(n);
p = buf;
for (i = 0; i < o->commit->nparent; i++)
p += sprint(p, "%H\n", o->commit->parent[i]);
readbuf(r, buf, n);
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 = 0444;
assert(qdir == Qcommit || qdir == Qobject || qdir == Qcommittree || qdir == Qhead);
if(strcmp(name, "msg") == 0)
q->path = qpath(p, 0, o->id, Qcommitmsg);
else if(strcmp(name, "parent") == 0)
q->path = qpath(p, 1, o->id, Qcommitparent);
else if(strcmp(name, "hash") == 0)
q->path = qpath(p, 2, o->id, Qcommithash);
else if(strcmp(name, "author") == 0)
q->path = qpath(p, 3, o->id, Qcommitauthor);
else if(strcmp(name, "tree") == 0){
q->type = QTDIR;
q->path = qpath(p, 4, o->id, Qcommittree);
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 "invalid object name";
if((c->obj = readobject(h)) == nil)
return "could not read object";
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 Qcommittree:
e = objwalk1(q, o->obj, o, c, name, Qcommittree, aux);
break;
case Qcommitparent:
case Qcommitmsg:
case Qcommitdata:
case Qcommithash:
case Qcommitauthor:
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 Qcommitmsg:
readbuf(r, o->commit->msg, o->commit->nmsg);
break;
case Qcommitparent:
readcommitparent(r, o);
break;
case Qcommithash:
snprint(buf, sizeof(buf), "%H\n", o->hash);
readstr(r, buf);
break;
case Qcommitauthor:
snprint(buf, sizeof(buf), "%s\n", o->commit->author);
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 Qcommittree:
case Qcommitdata:
objread(r, aux);
break;
default:
e = Egreg;
}
respond(r, e);
}
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(username);
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,
.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)
{
gitinit();
ARGBEGIN{
case 'd': chatty9p++; break;
default: usage(); break;
}ARGEND;
if(argc != 0)
usage();
username = getuser();
branches = emalloc(sizeof(char*));
branches[0] = nil;
postmountsrv(&gitsrv, nil, "/mnt/git", MCREATE);
exits(nil);
}

303
sys/src/cmd/git/git.h Normal file
View file

@ -0,0 +1,303 @@
#include <bio.h>
#include <mp.h>
#include <libsec.h>
#include <flate.h>
#include <regexp.h>
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;
enum {
Pathmax = 512,
Npackcache = 32,
Hashsz = 20,
Pktmax = 65536,
};
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 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 Hash Zhash;
extern int chattygit;
extern int cachemax;
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 *);

99
sys/src/cmd/git/import Executable file
View file

@ -0,0 +1,99 @@
#!/bin/rc
rfork ne
. /sys/lib/git/common.rc
diffpath=/tmp/gitimport.$pid.diff
fn sigexit {
rm -f $diffpath
}
fn apply @{
git/fs
email=''
name=''
msg=''
parents='-p'^`{git/query HEAD}
branch=`{git/branch}
if(test -e /mnt/git/branch/$branch/tree)
refpath=.git/refs/$branch
if not if(test -e /mnt/git/object/$branch/tree)
refpath=.git/HEAD
if not
die 'invalid branch:' $branch
awk '
BEGIN{
state="headers"
}
state=="headers" && /^From:/ {
sub(/^From:[ \t]*/, "", $0);
name=$0;
email=$0;
sub(/[ \t]*<.*$/, "", name);
sub(/.*</, "", email);
sub(/>/, "", email);
}
state=="headers" && /^Date:/{
sub(/^Date:[ \t]*/, "", $0)
date=$0
}
state=="headers" && /^Subject:/{
sub(/^Subject:[ \t]*(\[PATCH( [0-9]+\/[0-9]+)?\])*[ \t]*/, "", $0);
gotmsg = 1
print > "/env/msg"
}
state=="headers" && /^$/ {
state="body"
next
}
(state=="headers" || state=="body") && (/^diff/ || /^---[ ]*$/){
state="diff"
}
state=="body" {
print > "/env/msg"
}
state=="diff" {
print > ENVIRON["diffpath"]
}
END{
if(state != "diff")
exit("malformed patch: " state);
if(name == "" || email == "" || date == "" || gotmsg == "")
exit("missing headers");
printf "%s", name > "/env/name"
printf "%s", email > "/env/email"
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){
hash=`{git/save -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(f in $patches)
apply < $f || die $status
exit ''

38
sys/src/cmd/git/init Executable file
View file

@ -0,0 +1,38 @@
#!/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)
>$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 ''

329
sys/src/cmd/git/log.c Normal file
View file

@ -0,0 +1,329 @@
#include <u.h>
#include <libc.h>
#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;
Object **heap;
int nheap;
int heapsz;
Objset done;
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 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{
tmtime(&tm, o->commit->mtime, tzload("local"));
Bprint(out, "Hash:\t%H\n", o->hash);
Bprint(out, "Author:\t%s\n", o->commit->author);
Bprint(out, "Date:\t\n", tmfmt(&tm, "WW MMM D hh:mm:ss z YYYY"));
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
qput(Object *o)
{
Object *p;
int i;
if(oshas(&done, o->hash))
return;
osadd(&done, o);
if(nheap == heapsz){
heapsz *= 2;
heap = earealloc(heap, heapsz, sizeof(Object*));
}
heap[nheap++] = o;
for(i = nheap - 1; i > 0; i = (i-1)/2){
o = heap[i];
p = heap[(i-1)/2];
if(o->commit->mtime < p->commit->mtime)
break;
heap[i] = p;
heap[(i-1)/2] = o;
}
}
static Object*
qpop(void)
{
Object *o, *t;
int i, l, r, m;
if(nheap == 0)
return nil;
i = 0;
o = heap[0];
t = heap[--nheap];
heap[0] = t;
while(1){
m = i;
l = 2*i+1;
r = 2*i+2;
if(l < nheap && heap[m]->commit->mtime < heap[l]->commit->mtime)
m = l;
if(r < nheap && heap[m]->commit->mtime < heap[r]->commit->mtime)
m = r;
else
break;
t = heap[m];
heap[m] = heap[i];
heap[i] = t;
i = m;
}
return o;
}
static void
showcommits(char *c)
{
Object *o, *p;
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);
heapsz = 8;
heap = eamalloc(heapsz, sizeof(Object*));
osinit(&done);
qput(o);
while((o = qpop()) != nil){
show(o);
for(i = 0; i < o->commit->nparent; i++){
if((p = readobject(o->commit->parent[i])) == nil)
sysfatal("load %H: %r", o->commit->parent[i]);
qput(p);
}
unref(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;
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");
if(argc != 0){
if(getwd(path, sizeof(path)) == nil)
sysfatal("getwd: %r");
if(strlen(path) < strlen(repo))
sysfatal("path changed");
p = path + strlen(repo);
pathfilt = emalloc(sizeof(Pfilt));
for(i = 0; i < argc; i++){
r = smprint("./%s/%s", p, argv[i]);
cleanname(r);
filteradd(pathfilt, r);
free(r);
}
}
if(chdir(repo) == -1)
sysfatal("chdir: %r");
gitinit();
tmfmtinstall();
out = Bfdopen(1, OWRITE);
if(queryexpr != nil)
showquery(queryexpr);
else
showcommits(commitid);
exits(nil);
}

47
sys/src/cmd/git/merge Executable file
View file

@ -0,0 +1,47 @@
#!/bin/rc -e
rfork ne
. /sys/lib/git/common.rc
fn merge{
ourbr=/mnt/git/object/$1/tree
basebr=/mnt/git/object/$2/tree
theirbr=/mnt/git/object/$3/tree
all=`$nl{{git/query -c $1 $2; git/query -c $2 $3} | sed 's/^..//' | \
subst -g '^('$ourbr'|'$basebr'|'$theirbr')/*' | sort | uniq}
for(f in $all){
ours=$ourbr/$f
base=$basebr/$f
theirs=$theirbr/$f
merge1 $f $theirs $base $ours
}
}
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 ''

57
sys/src/cmd/git/mkfile Normal file
View file

@ -0,0 +1,57 @@
</$objtype/mkfile
BIN=/$objtype/bin/git
TARG=\
conf\
fetch\
fs\
log\
query\
repack\
save\
send\
serve\
walk
RC=\
add\
branch\
clone\
commit\
compat\
diff\
export\
import\
init\
merge\
pull\
push\
rebase\
revert\
rm
OFILES=\
delta.$O\
objset.$O\
ols.$O\
pack.$O\
proto.$O\
util.$O\
ref.$O
HFILES=git.h
</sys/src/cmd/mkmany
# Override install target to install rc.
install:V:
mkdir -p $BIN
mkdir -p /sys/lib/git
for (i in $TARG)
mk $MKFLAGS $i.install
for (i in $RC)
mk $MKFLAGS $i.rcinstall
%.rcinstall:V:
cp $stem $BIN/$stem
chmod +x $BIN/$stem

67
sys/src/cmd/git/objset.c Normal file
View file

@ -0,0 +1,67 @@
#include <u.h>
#include <libc.h>
#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;
}

170
sys/src/cmd/git/ols.c Normal file
View file

@ -0,0 +1,170 @@
#include <u.h>
#include <libc.h>
#include <ctype.h>
#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;
}

1712
sys/src/cmd/git/pack.c Normal file

File diff suppressed because it is too large Load diff

459
sys/src/cmd/git/proto.c Normal file
View file

@ -0,0 +1,459 @@
#include <u.h>
#include <libc.h>
#include <ctype.h>
#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 n;
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("dumb http protocol not supported");
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);
}else{
close(pfd[0]);
c->type = ConnSsh;
c->rfd = pfd[1];
c->wfd = dup(pfd[1], -1);
}
return 0;
}
static int
dialhjgit(Conn *c, char *host, char *port, char *path, char *direction, int auth)
{
char *ds, *p, *e, cmd[512];
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");
}else{
close(pfd[0]);
p = cmd;
e = cmd + sizeof(cmd);
p = seprint(p, e - 1, "git-%s-pack %s", direction, path);
p = seprint(p + 1, e, "host=%s", host);
c->type = ConnGit9;
c->rfd = pfd[1];
c->wfd = dup(pfd[1], -1);
if(writepkt(c, cmd, p - cmd + 1) == -1){
fprint(2, "failed to write message\n");
close(c->rfd);
close(c->wfd);
return -1;
}
}
return 0;
}
static int
dialgit(Conn *c, char *host, char *port, char *path, char *direction)
{
char *ds, *p, *e, cmd[512];
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;
p = cmd;
e = cmd + sizeof(cmd);
p = seprint(p, e - 1, "git-%s-pack %s", direction, path);
p = seprint(p + 1, e, "host=%s", host);
c->type = ConnGit;
c->rfd = fd;
c->wfd = dup(fd, -1);
if(writepkt(c, cmd, p - cmd + 1) == -1){
fprint(2, "failed to write message\n");
close(fd);
return -1;
}
return 0;
}
void
initconn(Conn *c, int rd, int wr)
{
c->type = ConnGit;
c->rfd = rd;
c->wfd = wr;
}
int
gitconnect(Conn *c, char *uri, char *direction)
{
char proto[Nproto], host[Nhost], port[Nport];
char repo[Nrepo], path[Npath];
if(parseuri(uri, proto, host, port, path, repo) == -1){
werrstr("bad uri %s", uri);
return -1;
}
memset(c, 0, sizeof(Conn));
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;
}
}

82
sys/src/cmd/git/pull Executable file
View file

@ -0,0 +1,82 @@
#!/bin/rc -e
rfork en
. /sys/lib/git/common.rc
fn update{
branch=$1
upstream=$2
url=$3
dir=$4
bflag=()
dflag=()
if(! ~ $#branch 0)
bflag=(-b $branch)
if(! ~ $#debug 0)
dflag='-d'
{git/fetch $dflag $bflag -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='a:allbranch, b:branch branch, d:debug,
f:fetchonly, u:upstream upstream, q:quiet'
args=''
eval `''{aux/getflags $*} || exec aux/usage
if(~ $#branch 0)
branch=refs/`{git/branch}
if(~ $allbranch 1)
branch=''
if(~ $#upstream 0)
upstream=origin
remote=`$nl{git/conf 'remote "'$upstream'".url'}
if(~ $#remote 0){
remote=$upstream
upstream=THEM
}
update $branch $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 >[1=2]
echo
echo $remote':' `{git/query $local} '=>' `{git/query $remote} >[1=2]
git/branch -mnb $remote $local
exit ''

51
sys/src/cmd/git/push Executable file
View file

@ -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 ''

196
sys/src/cmd/git/query.c Normal file
View file

@ -0,0 +1,196 @@
#include <u.h>
#include <libc.h>
#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;
char query[2048], repo[512];
ARGBEGIN{
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");
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 ? "/mnt/git/object/" : "");
for(j = 0; j < n; j++)
print("%s%H\n", p, h[reverse ? n - 1 - j : j]);
}
exits(nil);
}

92
sys/src/cmd/git/rebase Executable file
View file

@ -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 /mnt/git/HEAD/msg; echo; cat /mnt/git/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

677
sys/src/cmd/git/ref.c Normal file
View file

@ -0,0 +1,677 @@
#include <u.h>
#include <libc.h>
#include <ctype.h>
#include "git.h"
typedef struct Eval Eval;
typedef struct XObject XObject;
typedef struct Objq Objq;
enum {
Blank,
Keep,
Drop,
};
struct Eval {
char *str;
char *p;
Object **stk;
int nstk;
int stksz;
};
struct XObject {
Object *obj;
Object *mark;
XObject *queue;
XObject *next;
};
struct Objq {
Objq *next;
Object *o;
int color;
};
static Object zcommit = {
.type=GCommit
};
void
eatspace(Eval *ev)
{
while(isspace(ev->p[0]))
ev->p++;
}
int
objdatecmp(void *pa, void *pb)
{
Object *a, *b;
int r;
a = readobject((*(Object**)pa)->hash);
b = readobject((*(Object**)pb)->hash);
assert(a->type == GCommit && b->type == GCommit);
if(a->commit->mtime == b->commit->mtime)
r = 0;
else if(a->commit->mtime < b->commit->mtime)
r = -1;
else
r = 1;
unref(a);
unref(b);
return r;
}
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;
}
XObject*
hnode(XObject *ht[], Object *o)
{
XObject *h;
int hh;
hh = o->hash.h[0] & 0xff;
for(h = ht[hh]; h; h = h->next)
if(hasheq(&o->hash, &h->obj->hash))
return h;
h = emalloc(sizeof(*h));
h->obj = o;
h->mark = nil;
h->queue = nil;
h->next = ht[hh];
ht[hh] = h;
return h;
}
Object*
ancestor(Object *a, Object *b)
{
Object *o, *p, *r;
XObject *ht[256];
XObject *h, *q, *q1, *q2;
int i;
if(a == b)
return a;
if(a == nil || b == nil)
return nil;
r = nil;
memset(ht, 0, sizeof(ht));
q1 = nil;
h = hnode(ht, a);
h->mark = a;
h->queue = q1;
q1 = h;
h = hnode(ht, b);
h->mark = b;
h->queue = q1;
q1 = h;
while(1){
q2 = nil;
while(q = q1){
q1 = q->queue;
q->queue = nil;
o = q->obj;
for(i = 0; i < o->commit->nparent; i++){
p = readobject(o->commit->parent[i]);
if(p == nil)
goto err;
h = hnode(ht, p);
if(h->mark != nil){
if(h->mark != q->mark){
r = h->obj;
goto done;
}
} else {
h->mark = q->mark;
h->queue = q2;
q2 = h;
}
}
}
if(q2 == nil){
err:
werrstr("no common ancestor");
break;
}
q1 = q2;
}
done:
for(i=0; i<nelem(ht); i++){
while(h = ht[i]){
ht[i] = h->next;
free(h);
}
}
return r;
}
int
lca(Eval *ev)
{
Object *a, *b, *o;
if(ev->nstk < 2){
werrstr("ancestor needs 2 objects");
return -1;
}
a = pop(ev);
b = pop(ev);
o = ancestor(a, b);
if(o == nil)
return -1;
push(ev, o);
return 0;
}
static int
repaint(Objset *keep, Objset *drop, Object *o)
{
Object *p;
int i;
if(!oshas(keep, o->hash) && !oshas(drop, o->hash)){
dprint(2, "repaint: blank => drop %H\n", o->hash);
osadd(drop, o);
return 0;
}
if(oshas(keep, o->hash))
dprint(2, "repaint: keep => drop %H\n", o->hash);
osadd(drop, o);
for(i = 0; i < o->commit->nparent; i++){
if((p = readobject(o->commit->parent[i])) == nil)
return -1;
if(repaint(keep, drop, p) == -1)
return -1;
unref(p);
}
return 0;
}
int
findtwixt(Hash *head, int nhead, Hash *tail, int ntail, Object ***res, int *nres)
{
Objq *q, *e, *n, **p;
Objset keep, drop;
Object *o, *c;
int i, ncolor;
e = nil;
q = nil;
p = &q;
osinit(&keep);
osinit(&drop);
for(i = 0; i < nhead; i++){
if(hasheq(&head[i], &Zhash))
continue;
if((o = readobject(head[i])) == nil){
fprint(2, "warning: %H does not point at commit\n", 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, "twixt init: keep %H\n", o->hash);
e = emalloc(sizeof(Objq));
e->o = o;
e->color = Keep;
*p = e;
p = &e->next;
unref(o);
}
for(i = 0; i < ntail; i++){
if(hasheq(&tail[i], &Zhash))
continue;
if((o = readobject(tail[i])) == nil){
fprint(2, "warning: %H does not point at commit\n", o->hash);
werrstr("read tail %H: %r", tail[i]);
return -1;
}
if(o->type != GCommit){
unref(o);
continue;
}
dprint(1, "init: drop %H\n", o->hash);
e = emalloc(sizeof(Objq));
e->o = o;
e->color = Drop;
*p = e;
p = &e->next;
unref(o);
}
dprint(1, "finding twixt commits\n");
while(q != nil){
if(oshas(&drop, q->o->hash))
ncolor = Drop;
else if(oshas(&keep, q->o->hash))
ncolor = Keep;
else
ncolor = Blank;
if(ncolor == Drop || ncolor == Keep && q->color == Keep)
goto next;
if(ncolor == Keep && q->color == Drop){
if(repaint(&keep, &drop, q->o) == -1)
goto error;
}else if (ncolor == Blank) {
dprint(2, "visit: %s %H\n", q->color == Keep ? "keep" : "drop", q->o->hash);
if(q->color == Keep)
osadd(&keep, q->o);
else
osadd(&drop, q->o);
for(i = 0; i < q->o->commit->nparent; i++){
if((c = readobject(q->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, "enqueue: %s %H\n", q->color == Keep ? "keep" : "drop", c->hash);
n = emalloc(sizeof(Objq));
n->color = q->color;
n->next = nil;
n->o = c;
e->next = n;
e = n;
unref(c);
}
}else{
sysfatal("oops");
}
next:
n = q->next;
free(q);
q = n;
}
*res = eamalloc(keep.nobj, sizeof(Object*));
*nres = 0;
for(i = 0; i < keep.sz; i++){
if(keep.obj[i] != nil && !oshas(&drop, keep.obj[i]->hash)){
(*res)[*nres] = keep.obj[i];
(*nres)++;
}
}
osclear(&keep);
osclear(&drop);
return 0;
error:
for(; q != nil; q = n) {
n = q->next;
free(q);
}
return -1;
}
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, mark;
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;
mark = ev->nstk;
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);
qsort(ev->stk + mark, ev->nstk - mark, sizeof(Object*), objdatecmp);
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;
}

85
sys/src/cmd/git/repack.c Normal file
View file

@ -0,0 +1,85 @@
#include <u.h>
#include <libc.h>
#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);
}

19
sys/src/cmd/git/revert Normal file
View file

@ -0,0 +1,19 @@
#!/bin/rc
rfork e
. /sys/lib/git/common.rc
gitup
flagfmt='c:query query' args='file ...'
eval `''{aux/getflags $*} || exec aux/usage
commit=/mnt/git/HEAD
if(~ $#query 1)
commit=`{git/query -p $query}
for(f in `$nl{cd $commit/tree/ && walk -f ./$gitrel/$*}){
mkdir -p `{basename -d $f}
cp -- $commit/tree/$f $f
git/add $f
}
exit ''

3
sys/src/cmd/git/rm Executable file
View file

@ -0,0 +1,3 @@
#!/bin/rc -e
exec git/add -r $*

401
sys/src/cmd/git/save.c Normal file
View file

@ -0,0 +1,401 @@
#include <u.h>
#include <libc.h>
#include "git.h"
typedef struct Objbuf Objbuf;
struct Objbuf {
int off;
char *hdr;
int nhdr;
char *dat;
int ndat;
};
enum {
Maxparents = 16,
};
int
gitmode(int m)
{
if(m & DMDIR) /* directory */
return 0040000;
else if(m & 0111) /* executable */
return 0100755;
else if(m != 0) /* regular */
return 0100644;
else /* symlink */
return 0120000;
}
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->mode), 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, char *msg, char *name, char *email, vlong date, Hash *parents, int nparents, 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", name, email, date);
fmtprint(&f, "committer %s <%s> %lld +0000\n", name, email, date);
fmtprint(&f, "\n");
fmtprint(&f, "%s", msg);
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, parents[Maxparents];
char *msg, *name, *email, *dstr;
int i, r, nparents;
vlong date;
Object *t;
msg = nil;
name = nil;
email = nil;
dstr = nil;
date = time(nil);
nparents = 0;
gitinit();
ARGBEGIN{
case 'm': msg = EARGF(usage()); break;
case 'n': name = EARGF(usage()); break;
case 'e': email = 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();
}ARGEND;
if(!msg)
sysfatal("missing message");
if(!name)
sysfatal("missing name");
if(!email)
sysfatal("missing email");
if(dstr){
date=strtoll(dstr, &dstr, 10);
if(strlen(dstr) != 0)
sysfatal("could not parse date %s", dstr);
}
if(msg == nil || name == nil)
usage();
for(i = 0; i < argc; i++)
cleanname(argv[i]);
gitinit();
if(access(".git", AEXIST) != 0)
sysfatal("could not find git repo: %r");
t = findroot();
r = treeify(t, argv, argv + argc, 0, &th);
if(r == -1)
sysfatal("could not commit: %r\n");
mkcommit(&ch, msg, name, email, date, parents, nparents, th);
print("%H\n", ch);
exits(nil);
}

272
sys/src/cmd/git/send.c Normal file
View file

@ -0,0 +1,272 @@
#include <u.h>
#include <libc.h>
#include "git.h"
typedef struct Capset Capset;
struct Capset {
int sideband;
int sideband64k;
int report;
};
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
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, nupd, nsp, send, first;
char buf[Pktmax], *sp[3];
Hash h, *theirs, *ours;
Object *a, *b, *p;
char **refs;
Capset cs;
first = 1;
memset(&cs, 0, sizeof(Capset));
nupd = readours(&ours, &refs);
theirs = eamalloc(nupd, sizeof(Hash));
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);
if((idx = findref(refs, nupd, sp[1])) == -1)
continue;
if(hparse(&theirs[idx], sp[0]) == -1)
sysfatal("invalid hash %s", sp[0]);
}
if(writephase(c) == -1)
return -1;
send = 0;
if(force)
send=1;
for(i = 0; i < nupd; i++){
a = readobject(theirs[i]);
b = hasheq(&ours[i], &Zhash) ? nil : readobject(ours[i]);
p = nil;
if(a != nil && b != nil)
p = ancestor(a, b);
if(!force && !hasheq(&theirs[i], &Zhash) && (a == nil || p != a)){
fprint(2, "remote has diverged\n");
werrstr("force needed");
flushpkt(c);
return -1;
}
unref(a);
unref(b);
unref(p);
if(hasheq(&theirs[i], &ours[i])){
print("uptodate %s\n", refs[i]);
continue;
}
print("update %s %H %H\n", refs[i], theirs[i], ours[i]);
n = snprint(buf, sizeof(buf), "%H %H %s", theirs[i], ours[i], refs[i]);
/*
* 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, nupd, theirs, nupd, &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);
}

558
sys/src/cmd/git/serve.c Normal file
View file

@ -0,0 +1,558 @@
#include <u.h>
#include <libc.h>
#include <ctype.h>
#include <auth.h>
#include "git.h"
char *pathpfx = "/usr/git";
char *namespace = 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", 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];
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){
werrstr("repo locked\n");
return -1;
}
for(i = 0; i < nupd; i++){
if(resolveref(&h, ref[i]) == 0){
hadref = 1;
if(!hasheq(&h, &cur[i])){
werrstr("old ref changed: %s", ref[i]);
goto error;
}
}
if(snprint(refpath, sizeof(refpath), ".git/%s", ref[i]) == sizeof(refpath)){
werrstr("ref path too long: %s", ref[i]);
goto error;
}
if(hasheq(&upd[i], &Zhash)){
remove(refpath);
continue;
}
if((o = readobject(upd[i])) == nil){
werrstr("update to nonexistent hash %H", upd[i]);
goto error;
}
if(o->type != GCommit){
werrstr("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){
werrstr("open ref: %r");
goto error;
}
if(fprint(fd, "%H", upd[i]) == -1){
werrstr("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){
werrstr("open HEAD: %r");
goto error;
}
if(fprint(fd, "ref: %s", ref[0]) == -1){
werrstr("write HEAD ref: %r");
goto error;
}
close(fd);
}
ret = 0;
error:
fmtpkt(c, "ERR %r");
close(lockfd);
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];
char *user;
Conn c;
ARGBEGIN{
case 'd':
chattygit++;
break;
case 'r':
pathpfx = EARGF(usage());
if(*pathpfx != '/')
sysfatal("path prefix must begin with '/'");
break;
case 'n':
namespace=EARGF(usage());
break;
case 'w':
allowwrite++;
break;
default:
usage();
break;
}ARGEND;
gitinit();
user = "none";
interactive = 0;
if(allowwrite)
user = getuser();
if(newns(user, namespace) == -1)
sysfatal("addns: %r");
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);
}

321
sys/src/cmd/git/util.c Normal file
View file

@ -0,0 +1,321 @@
#include <u.h>
#include <libc.h>
#include <ctype.h>
#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;
if((na = (uvlong)n*(uvlong)sz) >= (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;
if((na = (uvlong)n*(uvlong)sz) >= (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;
}

333
sys/src/cmd/git/walk.c Normal file
View file

@ -0,0 +1,333 @@
#include <u.h>
#include <libc.h>
#include "git.h"
#define NCACHE 4096
#define TDIR ".git/index9/tracked"
#define RDIR ".git/index9/removed"
#define HDIR "/mnt/git/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; i<c->n; 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(access("/mnt/git/ctl", AEXIST) != 0)
sysfatal("no running git/fs");
if(findrepo(repo, sizeof(repo)) == -1)
sysfatal("find root: %r");
if(chdir(repo) == -1)
sysfatal("chdir: %r");
dirty = 0;
memset(&r, 0, sizeof(r));
if(access("/mnt/git/ctl", AEXIST) != 0)
sysfatal("git/fs does not seem to be running");
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);
}