Merge branch 'maint'
authorJunio C Hamano <junkio@cox.net>
Thu, 2 Mar 2006 07:20:31 +0000 (23:20 -0800)
committerJunio C Hamano <junkio@cox.net>
Thu, 2 Mar 2006 07:20:31 +0000 (23:20 -0800)
* maint:
  read-tree --aggressive: remove deleted entry from the working tree.

93 files changed:
.gitignore
Documentation/SubmittingPatches
Documentation/git-commit.txt
Documentation/git-cvsserver.txt [new file with mode: 0644]
Documentation/git-ls-files.txt
Documentation/git-rebase.txt
Documentation/git-rev-parse.txt
Documentation/git-rm.txt [new file with mode: 0644]
Documentation/git-send-email.txt
Documentation/git-svnimport.txt
Documentation/git-tag.txt
Documentation/git-update-index.txt
Makefile
apply.c
cache.h
cat-file.c
checkout-index.c
commit.c
commit.h
config.c
contrib/README [new file with mode: 0644]
contrib/emacs/git.el [new file with mode: 0644]
contrib/git-svn/.gitignore [new file with mode: 0644]
contrib/git-svn/Makefile [new file with mode: 0644]
contrib/git-svn/git-svn.perl [new file with mode: 0755]
contrib/git-svn/git-svn.txt [new file with mode: 0644]
contrib/git-svn/t/t0000-contrib-git-svn.sh [new file with mode: 0644]
contrib/gitview/gitview [new file with mode: 0755]
contrib/gitview/gitview.txt [new file with mode: 0644]
diff-delta.c
diff-files.c
diff-index.c
diff.c
diffcore-break.c
diffcore-delta.c [new file with mode: 0644]
diffcore-rename.c
diffcore.h
entry.c
environment.c
exec_cmd.c
fetch-pack.c
fsck-objects.c
git-add.sh
git-annotate.perl [new file with mode: 0755]
git-bisect.sh
git-clone.sh
git-commit.sh
git-cvsimport.perl
git-cvsserver.perl [new file with mode: 0755]
git-fetch.sh
git-fmt-merge-msg.perl
git-format-patch.sh
git-merge.sh
git-mv.perl
git-push.sh
git-rebase.sh
git-rerere.perl
git-revert.sh
git-rm.sh [new file with mode: 0755]
git-send-email.perl
git-svnimport.perl
git-tag.sh
git-verify-tag.sh
git.c
gitk
ls-files.c
mailinfo.c
merge-tree.c [new file with mode: 0644]
mktree.c [new file with mode: 0644]
pack-check.c
pack-objects.c
pack-redundant.c
read-cache.c
read-tree.c
refs.c
rev-list.c
rev-parse.c
send-pack.c
sha1_file.c
show-branch.c
t/Makefile
t/t0000-basic.sh
t/t3020-ls-files-error-unmatch.sh [new file with mode: 0755]
t/t3600-rm.sh [new file with mode: 0755]
t/t5600-clone-fail-cleanup.sh [new file with mode: 0755]
t/t6021-merge-criss-cross.sh
t/t6022-merge-rename.sh
t/t8001-annotate.sh [new file with mode: 0755]
t/test-lib.sh
templates/hooks--pre-rebase [new file with mode: 0644]
update-index.c
upload-pack.c
write-tree.c

index d7e8d2a..5be239a 100644 (file)
@@ -2,6 +2,7 @@ GIT-VERSION-FILE
 git
 git-add
 git-am
+git-annotate
 git-apply
 git-applymbox
 git-applypatch
@@ -22,6 +23,7 @@ git-convert-objects
 git-count-objects
 git-cvsexportcommit
 git-cvsimport
+git-cvsserver
 git-daemon
 git-diff
 git-diff-files
@@ -53,6 +55,7 @@ git-mailsplit
 git-merge
 git-merge-base
 git-merge-index
+git-merge-tree
 git-merge-octopus
 git-merge-one-file
 git-merge-ours
@@ -60,6 +63,7 @@ git-merge-recursive
 git-merge-resolve
 git-merge-stupid
 git-mktag
+git-mktree
 git-name-rev
 git-mv
 git-pack-redundant
@@ -84,6 +88,7 @@ git-resolve
 git-rev-list
 git-rev-parse
 git-revert
+git-rm
 git-send-email
 git-send-pack
 git-sh-setup
index 9ccb8f7..318b04f 100644 (file)
@@ -4,8 +4,8 @@ it for the core GIT to make sure people understand what they are
 doing when they write "Signed-off-by" line.
 
 But the patch submission requirements are a lot more relaxed
-here, because the core GIT is thousand times smaller ;-).  So
-here is only the relevant bits.
+here on the technical/contents front, because the core GIT is
+thousand times smaller ;-).  So here is only the relevant bits.
 
 
 (1) Make separate commits for logically separate changes.
@@ -18,13 +18,19 @@ repository.  It is a good discipline.
 
 Describe the technical detail of the change(s).
 
-If your description starts to get long, that's a sign that you
+If your description starts to get too long, that's a sign that you
 probably need to split up your commit to finer grained pieces.
 
+Oh, another thing.  I am picky about whitespaces.  Make sure your
+changes do not trigger errors with the sample pre-commit hook shipped
+in templates/hooks--pre-commit.
 
-(2) Generate your patch using git/cogito out of your commits.
 
-git diff tools generate unidiff which is the preferred format.
+(2) Generate your patch using git tools out of your commits.
+
+git based diff tools (git, Cogito, and StGIT included) generate
+unidiff which is the preferred format.
+
 You do not have to be afraid to use -M option to "git diff" or
 "git format-patch", if your patch involves file renames.  The
 receiving end can handle them just fine.
@@ -33,20 +39,22 @@ Please make sure your patch does not include any extra files
 which do not belong in a patch submission.  Make sure to review
 your patch after generating it, to ensure accuracy.  Before
 sending out, please make sure it cleanly applies to the "master"
-branch head.
+branch head.  If you are preparing a work based on "next" branch,
+that is fine, but please mark it as such.
 
 
 (3) Sending your patches.
 
-People on the git mailing list needs to be able to read and
+People on the git mailing list need to be able to read and
 comment on the changes you are submitting.  It is important for
 a developer to be able to "quote" your changes, using standard
 e-mail tools, so that they may comment on specific portions of
-your code.  For this reason, all patches should be submitting
-e-mail "inline".  WARNING: Be wary of your MUAs word-wrap
-corrupting your patch.  Do not cut-n-paste your patch.
+your code.  For this reason, all patches should be submited
+"inline".  WARNING: Be wary of your MUAs word-wrap
+corrupting your patch.  Do not cut-n-paste your patch; you can
+lose tabs that way if you are not careful.
 
-It is common convention to prefix your subject line with
+It is common convention to prefix your subject line with
 [PATCH].  This lets people easily distinguish patches from other
 e-mail discussions.
 
index 5b1b4d3..214ed23 100644 (file)
@@ -85,27 +85,12 @@ OPTIONS
 <file>...::
        Files to be committed.  The meaning of these is
        different between `--include` and `--only`.  Without
-       either, it defaults `--include` semantics.
+       either, it defaults `--only` semantics.
 
 If you make a commit and then found a mistake immediately after
 that, you can recover from it with gitlink:git-reset[1].
 
 
-WARNING
--------
-
-The 1.2.0 and its maintenance series 1.2.X will keep the
-traditional `--include` semantics as the default when neither
-`--only` nor `--include` is specified and `paths...` are given.
-This *will* change during the development towards 1.3.0 in the
-'master' branch of `git.git` repository.  If you are using this
-command in your scripts, and you depend on the traditional
-`--include` semantics, please update them to explicitly ask for
-`--include` semantics.  Also if you are used to making partial
-commit using `--include` semantics, please train your fingers to
-say `git commit --include paths...` (or `git commit -i paths...`).
-
-
 Discussion
 ----------
 
@@ -121,7 +106,7 @@ even the command is invoked from a subdirectory.
 That is, update the specified paths to the index and then commit
 the whole tree.
 
-`git commit --only paths...` largely bypasses the index file and
+`git commit paths...` largely bypasses the index file and
 commits only the changes made to the specified paths.  It has
 however several safety valves to prevent confusion.
 
diff --git a/Documentation/git-cvsserver.txt b/Documentation/git-cvsserver.txt
new file mode 100644 (file)
index 0000000..19c9c51
--- /dev/null
@@ -0,0 +1,113 @@
+git-cvsserver(1)
+================
+
+NAME
+----
+git-cvsserver - A CVS server emulator for git
+
+
+SYNOPSIS
+--------
+[verse]
+export CVS_SERVER=git-cvsserver
+'cvs' -d :ext:user@server/path/repo.git co <HEAD_name>
+
+
+DESCRIPTION
+-----------
+
+This application is a CVS emulation layer for git.
+
+It is highly functional. However, not all methods are implemented,
+and for those methods that are implemented,
+not all switches are implemented.
+
+Testing has been done using both the CLI CVS client, and the Eclipse CVS
+plugin. Most functionality works fine with both of these clients.
+
+LIMITATIONS
+-----------
+Currently gitcvs only works over ssh connections.
+
+
+INSTALLATION
+------------
+1. Put server.pl somewhere useful on the same machine that is hosting your git repos
+
+2. For each repo that you want accessible from CVS you need to edit config in
+   the repo and add the following section.
+
+   [gitcvs]
+        enabled=1
+        logfile=/path/to/logfile
+
+   n.b. you need to ensure each user that is going to invoke server.pl has
+   write access to the log file.
+
+5. On each client machine you need to set the following variables.
+     CVSROOT should be set as per normal, but the directory should point at the
+             appropriate git repo.
+     CVS_SERVER should be set to the server.pl script that has been put on the
+                remote machine.
+
+6. Clients should now be able to check out modules (where modules are the names
+   of branches in git).
+     $ cvs co -d mylocaldir master
+
+Eclipse CVS Client Notes
+------------------------
+
+To get a checkout with the Eclipse CVS client:
+
+1. Create a new project from CVS checkout, giving it repository and module
+2. Context Menu->Team->Share Project...
+3. Enter the repository and module information again and click Finish
+4. The Synchronize view appears. Untick  "launch commit wizard" to avoid
+committing the .project file, and select HEAD as the tag to synchronize to.
+Update all incoming changes.
+
+Note that most versions of Eclipse ignore CVS_SERVER (which you can set in
+the Preferences->Team->CVS->ExtConnection pane), so you may have to
+rename, alias or symlink git-cvsserver to 'cvs' on the server.
+
+Clients known to work
+---------------------
+
+CVS 1.12.9 on Debian
+CVS 1.11.17 on MacOSX (from Fink package)
+Eclipse 3.0, 3.1.2 on MacOSX (see Eclipse CVS Client Notes)
+TortoiseCVS
+
+Operations supported
+--------------------
+
+All the operations required for normal use are supported, including
+checkout, diff, status, update, log, add, remove, commit.
+Legacy monitoring operations are not supported (edit, watch and related).
+Exports and tagging (tags and branches) are not supported at this stage.
+
+The server will set the -k mode to binary when relevant. In proper GIT
+tradition, the contents of the files are always respected.
+No keyword expansion or newline munging is supported.
+
+Dependencies
+------------
+
+git-cvsserver depends on DBD::SQLite.
+
+Copyright and Authors
+---------------------
+
+This program is copyright The Open University UK - 2006.
+
+Authors: Martyn Smith    <martyn@catalyst.net.nz>
+         Martin Langhoff <martin@catalyst.net.nz>
+         with ideas and patches from participants of the git-list <git@vger.kernel.org>.
+
+Documentation
+--------------
+Documentation by Martyn Smith <martyn@catalyst.net.nz> and Martin Langhoff <martin@catalyst.net.nz>Matthias Urlichs <smurf@smurf.noris.de>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
index fe53412..e813f84 100644 (file)
@@ -8,12 +8,14 @@ git-ls-files - Information about files in the index/working directory
 
 SYNOPSIS
 --------
-'git-ls-files' [-z] [-t]
+[verse]
+'git-ls-files' [-z] [-t] [-v]
                (--[cached|deleted|others|ignored|stage|unmerged|killed|modified])\*
                (-[c|d|o|i|s|u|k|m])\*
                [-x <pattern>|--exclude=<pattern>]
                [-X <file>|--exclude-from=<file>]
                [--exclude-per-directory=<file>] 
+               [--error-unmatch]
                [--full-name] [--] [<file>]\*
 
 DESCRIPTION
@@ -72,6 +74,10 @@ OPTIONS
        read additional exclude patterns that apply only to the
        directory and its subdirectories in <file>.
 
+--error-unmatch::
+       If any <file> does not appear in the index, treat this as an
+       error (return 1).
+
 -t::
        Identify the file status with the following tags (followed by
        a space) at the start of each line:
@@ -82,6 +88,10 @@ OPTIONS
        K::     to be killed
        ?::     other
 
+-v::
+       Similar to `-t`, but use lowercase letters for files
+       that are marked as 'always matching index'.
+
 --full-name::
        When run from a subdirectory, the command usually
        outputs paths relative to the current directory.  This
index 16c158f..f037d12 100644 (file)
@@ -7,14 +7,54 @@ git-rebase - Rebase local commits to new upstream head.
 
 SYNOPSIS
 --------
-'git-rebase' <upstream> [<head>]
+'git-rebase' [--onto <newbase>] <upstream> [<branch>]
 
 DESCRIPTION
 -----------
-Rebases local commits to the new head of the upstream tree.
+git-rebase applies to <upstream> (or optionally to <newbase>) commits
+from <branch> that do not appear in <upstream>. When <branch> is not
+specified it defaults to the current branch (HEAD).
+
+When git-rebase is complete, <branch> will be updated to point to the
+newly created line of commit objects, so the previous line will not be
+accessible unless there are other references to it already.
+
+Assume the following history exists and the current branch is "topic":
+
+          A---B---C topic
+         /
+    D---E---F---G master
+
+From this point, the result of the following commands:
+
+    git-rebase master
+    git-rebase master topic
+
+would be:
+
+                  A'--B'--C' topic
+                 /
+    D---E---F---G master
+
+While, starting from the same point, the result of the following
+commands:
+
+    git-rebase --onto master~1 master
+    git-rebase --onto master~1 master topic
+
+would be:
+
+              A'--B'--C' topic
+             /
+    D---E---F---G master
 
 OPTIONS
 -------
+<newbase>::
+       Starting point at which to create the new commits. If the
+       --onto option is not specified, the starting point is
+       <upstream>.
+
 <upstream>::
        Upstream branch to compare against.
 
index 1662e06..29b5789 100644 (file)
@@ -80,7 +80,7 @@ OPTIONS
 --git-dir::
        Show `$GIT_DIR` if defined else show the path to the .git directory.
 
---short, short=number::
+--short, --short=number::
        Instead of outputting the full SHA1 values of object names try to
        abbriviate them to a shorter unique name. When no length is specified
        7 is used. The minimum length is 4.
diff --git a/Documentation/git-rm.txt b/Documentation/git-rm.txt
new file mode 100644 (file)
index 0000000..401bfb2
--- /dev/null
@@ -0,0 +1,89 @@
+git-rm(1)
+=========
+
+NAME
+----
+git-rm - Remove files from the working tree and from the index.
+
+SYNOPSIS
+--------
+'git-rm' [-f] [-n] [-v] [--] <file>...
+
+DESCRIPTION
+-----------
+A convenience wrapper for git-update-index --remove. For those coming
+from cvs, git-rm provides an operation similar to "cvs rm" or "cvs
+remove".
+
+
+OPTIONS
+-------
+<file>...::
+       Files to remove from the index and optionally, from the
+       working tree as well.
+
+-f::
+       Remove files from the working tree as well as from the index.
+
+-n::
+        Don't actually remove the file(s), just show if they exist in
+        the index.
+
+-v::
+        Be verbose.
+
+--::
+       This option can be used to separate command-line options from
+       the list of files, (useful when filenames might be mistaken
+       for command-line options).
+
+
+DISCUSSION
+----------
+
+The list of <file> given to the command is fed to `git-ls-files`
+command to list files that are registered in the index and
+are not ignored/excluded by `$GIT_DIR/info/exclude` file or
+`.gitignore` file in each directory.  This means two things:
+
+. You can put the name of a directory on the command line, and the
+  command will remove all files in it and its subdirectories (the
+  directories themselves are never removed from the working tree);
+
+. Giving the name of a file that is not in the index does not
+  remove that file.
+
+
+EXAMPLES
+--------
+git-rm Documentation/\\*.txt::
+
+       Removes all `\*.txt` files from the index that are under the
+       `Documentation` directory and any of its subdirectories. The
+       files are not removed from the working tree.
++
+Note that the asterisk `\*` is quoted from the shell in this
+example; this lets the command include the files from
+subdirectories of `Documentation/` directory.
+
+git-rm -f git-*.sh::
+
+       Remove all git-*.sh scripts that are in the index. The files
+       are removed from the index, and (because of the -f option),
+       from the working tree as well. Because this example lets the
+       shell expand the asterisk (i.e. you are listing the files
+       explicitly), it does not remove `subdir/git-foo.sh`.
+
+
+Author
+------
+Written by Linus Torvalds <torvalds@osdl.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>.
+
+GIT
+---
+Part of the gitlink:git[7] suite
+
index 00537d8..8c58685 100644 (file)
@@ -24,6 +24,9 @@ OPTIONS
 -------
 The options available are:
 
+--cc::
+       Specify a starting "Cc:" value for each email.
+
 --chain-reply-to, --no-chain-reply-to::
        If this is set, each email will be sent as a reply to the previous
        email sent.  If disabled with "--no-chain-reply-to", all emails after
@@ -48,6 +51,9 @@ The options available are:
        Only necessary if --compose is also set.  If --compose
        is not set, this will be prompted for.
 
+--no-signed-off-by-cc::
+       Do not add emails foudn in Signed-off-by: lines to the cc list.
+
 --quiet::
        Make git-send-email less verbose.  One line per email should be
        all that is output.
@@ -61,6 +67,10 @@ The options available are:
        Only necessary if --compose is also set.  If --compose
        is not set, this will be prompted for.
 
+--suppress-from::
+       Do not add the From: address to the cc: list, if it shows up in a From:
+       line.
+
 --to::
        Specify the primary recipient of the emails generated.
        Generally, this will be the upstream maintainer of the
index 63e28b8..a158813 100644 (file)
@@ -10,10 +10,11 @@ git-svnimport - Import a SVN repository into git
 SYNOPSIS
 --------
 'git-svnimport' [ -o <branch-for-HEAD> ] [ -h ] [ -v ] [ -d | -D ]
-                       [ -C <GIT_repository> ] [ -i ] [ -u ] [-l limit_rev]
-                       [ -b branch_subdir ] [ -T trunk_subdir ] [ -t tag_subdir ]
-                       [ -s start_chg ] [ -m ] [ -M regex ]
-                       <SVN_repository_URL> [ <path> ]
+               [ -C <GIT_repository> ] [ -i ] [ -u ] [-l limit_rev]
+               [ -b branch_subdir ] [ -T trunk_subdir ] [ -t tag_subdir ]
+               [ -s start_chg ] [ -m ] [ -r ] [ -M regex ]
+               [ -I <ignorefile_name> ] [ -A <author_file> ]
+               <SVN_repository_URL> [ <path> ]
 
 
 DESCRIPTION
@@ -61,6 +62,31 @@ When importing incrementally, you might need to edit the .git/svn2git file.
        the git repository. Use this option if you want to import into a
        different branch.
 
+-r::
+       Prepend 'rX: ' to commit messages, where X is the imported
+       subversion revision.
+
+-I <ignorefile_name>::
+       Import the svn:ignore directory property to files with this
+       name in each directory. (The Subversion and GIT ignore
+       syntaxes are similar enough that using the Subversion patterns
+       directly with "-I .gitignore" will almost always just work.)
+
+-A <author_file>::
+       Read a file with lines on the form
+
+         username = User's Full Name <email@addr.es>
+
+       and use "User's Full Name <email@addr.es>" as the GIT
+       author and committer for Subversion commits made by
+       "username". If encountering a commit made by a user not in the
+       list, abort.
+
+       For convenience, this data is saved to $GIT_DIR/svn-authors
+       each time the -A option is provided, and read from that same
+       file each time git-svnimport is run with an existing GIT
+       repository without -A.
+
 -m::
        Attempt to detect merges based on the commit message. This option
        will enable default regexes that try to capture the name source
index e8892bb..e1c76c6 100644 (file)
@@ -8,11 +8,13 @@ git-tag -  Create a tag object signed with GPG
 
 SYNOPSIS
 --------
+[verse]
 'git-tag' [-a | -s | -u <key-id>] [-f | -d] [-m <msg>] <name> [<head>]
+'git-tag' -l [<pattern>]
 
 DESCRIPTION
 -----------
-Adds a 'tag' reference in .git/refs/tags/
+Adds a 'tag' reference in `.git/refs/tags/`
 
 Unless `-f` is given, the tag must not yet exist in
 `.git/refs/tags/` directory.
@@ -32,6 +34,9 @@ GnuPG key for signing.
 
 `-d <tag>` deletes the tag.
 
+`-l <pattern>` lists tags that match the given pattern (or all
+if no pattern is given).
+
 OPTIONS
 -------
 -a::
@@ -49,6 +54,9 @@ OPTIONS
 -d::
        Delete an existing tag with the given name
 
+-l <pattern>::
+       List tags that match the given pattern (or all if no pattern is given).
+
 -m <msg>::
        Use the given tag message (instead of prompting)
 
index c74311d..0a1b0ad 100644 (file)
@@ -8,11 +8,14 @@ git-update-index - Modifies the index or directory cache
 
 SYNOPSIS
 --------
+[verse]
 'git-update-index'
             [--add] [--remove | --force-remove] [--replace] 
             [--refresh [-q] [--unmerged] [--ignore-missing]]
             [--cacheinfo <mode> <object> <file>]\*
             [--chmod=(+|-)x]
+            [--assume-unchanged | --no-assume-unchanged]
+            [--really-refresh]
             [--info-only] [--index-info]
             [-z] [--stdin]
             [--verbose]
@@ -65,6 +68,18 @@ OPTIONS
 --chmod=(+|-)x::
         Set the execute permissions on the updated files.        
 
+--assume-unchanged, --no-assume-unchanged::
+       When these flags are specified, the object name recorded
+       for the paths are not updated.  Instead, these options
+       sets and unsets the "assume unchanged" bit for the
+       paths.  When the "assume unchanged" bit is on, git stops
+       checking the working tree files for possible
+       modifications, so you need to manually unset the bit to
+       tell git when you change the working tree file. This is
+       sometimes helpful when working with a big project on a
+       filesystem that has very slow lstat(2) system call
+       (e.g. cifs).
+
 --info-only::
        Do not create objects in the object database for all
        <file> arguments that follow this flag; just insert
@@ -193,6 +208,37 @@ $ git ls-files -s
 ------------
 
 
+Using "assume unchanged" bit
+----------------------------
+
+Many operations in git depend on your filesystem to have an
+efficient `lstat(2)` implementation, so that `st_mtime`
+information for working tree files can be cheaply checked to see
+if the file contents have changed from the version recorded in
+the index file.  Unfortunately, some filesystems have
+inefficient `lstat(2)`.  If your filesystem is one of them, you
+can set "assume unchanged" bit to paths you have not changed to
+cause git not to do this check.  Note that setting this bit on a
+path does not mean git will check the contents of the file to
+see if it has changed -- it makes git to omit any checking and
+assume it has *not* changed.  When you make changes to working
+tree files, you have to explicitly tell git about it by dropping
+"assume unchanged" bit, either before or after you modify them.
+
+In order to set "assume unchanged" bit, use `--assume-unchanged`
+option.  To unset, use `--no-assume-unchanged`.
+
+The command looks at `core.ignorestat` configuration variable.  When
+this is true, paths updated with `git-update-index paths...` and
+paths updated with other git commands that update both index and
+working tree (e.g. `git-apply --index`, `git-checkout-index -u`,
+and `git-read-tree -u`) are automatically marked as "assume
+unchanged".  Note that "assume unchanged" bit is *not* set if
+`git-update-index --refresh` finds the working tree file matches
+the index (use `git-update-index --really-refresh` if you want
+to mark them as "assume unchanged").
+
+
 Examples
 --------
 To update and refresh only the files already checked out:
@@ -201,6 +247,35 @@ To update and refresh only the files already checked out:
 $ git-checkout-index -n -f -a && git-update-index --ignore-missing --refresh
 ----------------
 
+On an inefficient filesystem with `core.ignorestat` set:
+
+------------
+$ git update-index --really-refresh <1>
+$ git update-index --no-assume-unchanged foo.c <2>
+$ git diff --name-only <3>
+$ edit foo.c
+$ git diff --name-only <4>
+M foo.c
+$ git update-index foo.c <5>
+$ git diff --name-only <6>
+$ edit foo.c
+$ git diff --name-only <7>
+$ git update-index --no-assume-unchanged foo.c <8>
+$ git diff --name-only <9>
+M foo.c
+
+<1> forces lstat(2) to set "assume unchanged" bits for paths
+    that match index.
+<2> mark the path to be edited.
+<3> this does lstat(2) and finds index matches the path.
+<4> this does lstat(2) and finds index does not match the path.
+<5> registering the new version to index sets "assume unchanged" bit.
+<6> and it is assumed unchanged.
+<7> even after you edit it.
+<8> you can tell about the change after the fact.
+<9> now it checks with lstat(2) and finds it has been changed.
+------------
+
 
 Configuration
 -------------
@@ -213,6 +288,9 @@ in the index and the file mode on the filesystem if they differ only on
 executable bit.   On such an unfortunate filesystem, you may
 need to use `git-update-index --chmod=`.
 
+The command looks at `core.ignorestat` configuration variable.  See
+'Using "assume unchanged" bit' section above.
+
 
 See Also
 --------
index 648469e..a5eb0c4 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -53,6 +53,13 @@ all:
 # Define NO_SOCKADDR_STORAGE if your platform does not have struct
 # sockaddr_storage.
 #
+# Define NO_ICONV if your libc does not properly support iconv.
+#
+# Define NO_ACCURATE_DIFF if your diff program at least sometimes misses
+# a missing newline at the end of the file.
+#
+# Define NO_PYTHON if you want to loose all benefits of the recursive merge.
+#
 # Define COLLISION_CHECK below if you believe that SHA1's
 # 1461501637330902918203684832716283019655932542976 hashes do not give you
 # sufficient guarantee that no collisions between objects will ever happen.
@@ -70,6 +77,12 @@ GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE
        @$(SHELL_PATH) ./GIT-VERSION-GEN
 -include GIT-VERSION-FILE
 
+uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
+uname_M := $(shell sh -c 'uname -m 2>/dev/null || echo not')
+uname_O := $(shell sh -c 'uname -o 2>/dev/null || echo not')
+uname_R := $(shell sh -c 'uname -r 2>/dev/null || echo not')
+uname_P := $(shell sh -c 'uname -p 2>/dev/null || echo not')
+
 # CFLAGS and LDFLAGS are for the users to override from the command line.
 
 CFLAGS = -g -O2 -Wall
@@ -80,7 +93,7 @@ STRIP ?= strip
 
 prefix = $(HOME)
 bindir = $(prefix)/bin
-gitexecdir = $(prefix)/bin
+gitexecdir = $(bindir)
 template_dir = $(prefix)/share/git-core/templates/
 GIT_PYTHON_DIR = $(prefix)/share/git-core/python
 # DESTDIR=
@@ -107,7 +120,7 @@ SCRIPT_SH = \
        git-merge-one-file.sh git-parse-remote.sh \
        git-prune.sh git-pull.sh git-push.sh git-rebase.sh \
        git-repack.sh git-request-pull.sh git-reset.sh \
-       git-resolve.sh git-revert.sh git-sh-setup.sh \
+       git-resolve.sh git-revert.sh git-rm.sh git-sh-setup.sh \
        git-tag.sh git-verify-tag.sh git-whatchanged.sh \
        git-applymbox.sh git-applypatch.sh git-am.sh \
        git-merge.sh git-merge-stupid.sh git-merge-octopus.sh \
@@ -117,6 +130,7 @@ SCRIPT_SH = \
 SCRIPT_PERL = \
        git-archimport.perl git-cvsimport.perl git-relink.perl \
        git-shortlog.perl git-fmt-merge-msg.perl git-rerere.perl \
+       git-annotate.perl git-cvsserver.perl \
        git-svnimport.perl git-mv.perl git-cvsexportcommit.perl
 
 SCRIPT_PYTHON = \
@@ -129,7 +143,7 @@ SCRIPTS = $(patsubst %.sh,%,$(SCRIPT_SH)) \
 
 # The ones that do not have to link with lcrypto nor lz.
 SIMPLE_PROGRAMS = \
-       git-get-tar-commit-id$X git-mailinfo$X git-mailsplit$X \
+       git-get-tar-commit-id$X git-mailsplit$X \
        git-stripspace$X git-daemon$X
 
 # ... and all the rest that could be moved out of bindir to gitexecdir
@@ -139,9 +153,9 @@ PROGRAMS = \
        git-convert-objects$X git-diff-files$X \
        git-diff-index$X git-diff-stages$X \
        git-diff-tree$X git-fetch-pack$X git-fsck-objects$X \
-       git-hash-object$X git-index-pack$X git-init-db$X \
-       git-local-fetch$X git-ls-files$X git-ls-tree$X git-merge-base$X \
-       git-merge-index$X git-mktag$X git-pack-objects$X git-patch-id$X \
+       git-hash-object$X git-index-pack$X git-init-db$X git-local-fetch$X \
+       git-ls-files$X git-ls-tree$X git-mailinfo$X git-merge-base$X \
+       git-merge-index$X git-mktag$X git-mktree$X git-pack-objects$X git-patch-id$X \
        git-peek-remote$X git-prune-packed$X git-read-tree$X \
        git-receive-pack$X git-rev-list$X git-rev-parse$X \
        git-send-pack$X git-show-branch$X git-shell$X \
@@ -151,7 +165,7 @@ PROGRAMS = \
        git-upload-pack$X git-verify-pack$X git-write-tree$X \
        git-update-ref$X git-symbolic-ref$X git-check-ref-format$X \
        git-name-rev$X git-pack-redundant$X git-repo-config$X git-var$X \
-       git-describe$X
+       git-describe$X git-merge-tree$X
 
 # what 'all' will build and 'install' will install, in gitexecdir
 ALL_PROGRAMS = $(PROGRAMS) $(SIMPLE_PROGRAMS) $(SCRIPTS)
@@ -182,7 +196,8 @@ LIB_H = \
 
 DIFF_OBJS = \
        diff.o diffcore-break.o diffcore-order.o diffcore-pathspec.o \
-       diffcore-pickaxe.o diffcore-rename.o tree-diff.o combine-diff.o
+       diffcore-pickaxe.o diffcore-rename.o tree-diff.o combine-diff.o \
+       diffcore-delta.o
 
 LIB_OBJS = \
        blob.o commit.o connect.o count-delta.o csum-file.o \
@@ -197,12 +212,6 @@ LIB_OBJS = \
 LIBS = $(LIB_FILE)
 LIBS += -lz
 
-# Shell quote;
-# Result of this needs to be placed inside ''
-shq = $(subst ','\'',$(1))
-# This has surrounding ''
-shellquote = '$(call shq,$(1))'
-
 #
 # Platform specific tweaks
 #
@@ -210,28 +219,28 @@ shellquote = '$(call shq,$(1))'
 # We choose to avoid "if .. else if .. else .. endif endif"
 # because maintaining the nesting to match is a pain.  If
 # we had "elif" things would have been much nicer...
-uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
-uname_M := $(shell sh -c 'uname -m 2>/dev/null || echo not')
-uname_O := $(shell sh -c 'uname -o 2>/dev/null || echo not')
-uname_R := $(shell sh -c 'uname -r 2>/dev/null || echo not')
 
 ifeq ($(uname_S),Darwin)
        NEEDS_SSL_WITH_CRYPTO = YesPlease
        NEEDS_LIBICONV = YesPlease
        ## fink
-       ALL_CFLAGS += -I/sw/include
-       ALL_LDFLAGS += -L/sw/lib
+       ifeq ($(shell test -d /sw/lib && echo y),y)
+               ALL_CFLAGS += -I/sw/include
+               ALL_LDFLAGS += -L/sw/lib
+       endif
        ## darwinports
-       ALL_CFLAGS += -I/opt/local/include
-       ALL_LDFLAGS += -L/opt/local/lib
+       ifeq ($(shell test -d /opt/local/lib && echo y),y)
+               ALL_CFLAGS += -I/opt/local/include
+               ALL_LDFLAGS += -L/opt/local/lib
+       endif
 endif
 ifeq ($(uname_S),SunOS)
        NEEDS_SOCKET = YesPlease
        NEEDS_NSL = YesPlease
-       NEEDS_LIBICONV = YesPlease
        SHELL_PATH = /bin/bash
        NO_STRCASESTR = YesPlease
        ifeq ($(uname_R),5.8)
+               NEEDS_LIBICONV = YesPlease
                NO_UNSETENV = YesPlease
                NO_SETENV = YesPlease
        endif
@@ -271,6 +280,16 @@ ifeq ($(uname_S),AIX)
        NO_STRCASESTR=YesPlease
        NEEDS_LIBICONV=YesPlease
 endif
+ifeq ($(uname_S),IRIX64)
+       NO_IPV6=YesPlease
+       NO_SETENV=YesPlease
+       NO_STRCASESTR=YesPlease
+       NO_SOCKADDR_STORAGE=YesPlease
+       SHELL_PATH=/usr/gnu/bin/bash
+       ALL_CFLAGS += -DPATH_MAX=1024
+       # for now, build 32-bit version
+       ALL_LDFLAGS += -L/usr/lib32
+endif
 ifneq (,$(findstring arm,$(uname_M)))
        ARM_SHA1 = YesPlease
 endif
@@ -280,8 +299,10 @@ endif
 ifdef WITH_OWN_SUBPROCESS_PY
        PYMODULES += compat/subprocess.py
 else
-       ifneq ($(shell $(PYTHON_PATH) -c 'import subprocess;print"OK"' 2>/dev/null),OK)
-               PYMODULES += compat/subprocess.py
+       ifeq ($(NO_PYTHON),)
+               ifneq ($(shell $(PYTHON_PATH) -c 'import subprocess;print"OK"' 2>/dev/null),OK)
+                       PYMODULES += compat/subprocess.py
+               endif
        endif
 endif
 
@@ -380,6 +401,10 @@ else
 endif
 endif
 
+ifdef NO_ICONV
+       ALL_CFLAGS += -DNO_ICONV
+endif
+
 ifdef PPC_SHA1
        SHA1_HEADER = "ppc/sha1.h"
        LIB_OBJS += ppc/sha1.o ppc/sha1ppc.o
@@ -397,8 +422,25 @@ else
 endif
 endif
 endif
+ifdef NO_ACCURATE_DIFF
+       ALL_CFLAGS += -DNO_ACCURATE_DIFF
+endif
+
+# Shell quote (do not use $(call) to accomodate ancient setups);
 
-ALL_CFLAGS += -DSHA1_HEADER=$(call shellquote,$(SHA1_HEADER)) $(COMPAT_CFLAGS)
+SHA1_HEADER_SQ = $(subst ','\'',$(SHA1_HEADER))
+
+DESTDIR_SQ = $(subst ','\'',$(DESTDIR))
+bindir_SQ = $(subst ','\'',$(bindir))
+gitexecdir_SQ = $(subst ','\'',$(gitexecdir))
+template_dir_SQ = $(subst ','\'',$(template_dir))
+
+SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
+PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH))
+PYTHON_PATH_SQ = $(subst ','\'',$(PYTHON_PATH))
+GIT_PYTHON_DIR_SQ = $(subst ','\'',$(GIT_PYTHON_DIR))
+
+ALL_CFLAGS += -DSHA1_HEADER='$(SHA1_HEADER_SQ)' $(COMPAT_CFLAGS)
 LIB_OBJS += $(COMPAT_OBJS)
 export prefix TAR INSTALL DESTDIR SHELL_PATH template_dir
 ### Build rules
@@ -417,23 +459,24 @@ git$X: git.c $(LIB_FILE)
 
 $(patsubst %.sh,%,$(SCRIPT_SH)) : % : %.sh
        rm -f $@
-       sed -e '1s|#!.*/sh|#!$(call shq,$(SHELL_PATH))|' \
+       sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \
            -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
            -e 's/@@NO_CURL@@/$(NO_CURL)/g' \
+           -e 's/@@NO_PYTHON@@/$(NO_PYTHON)/g' \
            $@.sh >$@
        chmod +x $@
 
 $(patsubst %.perl,%,$(SCRIPT_PERL)) : % : %.perl
        rm -f $@
-       sed -e '1s|#!.*perl|#!$(call shq,$(PERL_PATH))|' \
+       sed -e '1s|#!.*perl|#!$(PERL_PATH_SQ)|' \
            -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
            $@.perl >$@
        chmod +x $@
 
 $(patsubst %.py,%,$(SCRIPT_PYTHON)) : % : %.py
        rm -f $@
-       sed -e '1s|#!.*python|#!$(call shq,$(PYTHON_PATH))|' \
-           -e 's|@@GIT_PYTHON_PATH@@|$(call shq,$(GIT_PYTHON_DIR))|g' \
+       sed -e '1s|#!.*python|#!$(PYTHON_PATH_SQ)|' \
+           -e 's|@@GIT_PYTHON_PATH@@|$(GIT_PYTHON_DIR_SQ)|g' \
            -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
            $@.py >$@
        chmod +x $@
@@ -459,32 +502,42 @@ git$X git.spec \
 %.o: %.S
        $(CC) -o $*.o -c $(ALL_CFLAGS) $<
 
-exec_cmd.o: ALL_CFLAGS += -DGIT_EXEC_PATH=\"$(gitexecdir)\"
+exec_cmd.o: exec_cmd.c
+       $(CC) -o $*.o -c $(ALL_CFLAGS) '-DGIT_EXEC_PATH="$(gitexecdir_SQ)"' $<
 
 git-%$X: %.o $(LIB_FILE)
        $(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)
 
-git-mailinfo$X : SIMPLE_LIB += $(LIB_4_ICONV)
 $(SIMPLE_PROGRAMS) : $(LIB_FILE)
 $(SIMPLE_PROGRAMS) : git-%$X : %.o
        $(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \
                $(LIB_FILE) $(SIMPLE_LIB)
 
-git-http-fetch$X: fetch.o http.o
-git-http-push$X: http.o
+git-mailinfo$X: mailinfo.o $(LIB_FILE)
+       $(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \
+               $(LIB_FILE) $(SIMPLE_LIB) $(LIB_4_ICONV)
+
 git-local-fetch$X: fetch.o
 git-ssh-fetch$X: rsh.o fetch.o
 git-ssh-upload$X: rsh.o
 git-ssh-pull$X: rsh.o fetch.o
 git-ssh-push$X: rsh.o
 
-git-http-fetch$X: LIBS += $(CURL_LIBCURL)
-git-http-push$X: LIBS += $(CURL_LIBCURL) $(EXPAT_LIBEXPAT)
-git-rev-list$X: LIBS += $(OPENSSL_LIBSSL)
+git-http-fetch$X: fetch.o http.o http-fetch.o $(LIB_FILE)
+       $(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \
+               $(LIBS) $(CURL_LIBCURL)
+
+git-http-push$X: http.o http-push.o $(LIB_FILE)
+       $(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \
+               $(LIBS) $(CURL_LIBCURL) $(EXPAT_LIBEXPAT)
+
+git-rev-list$X: rev-list.o $(LIB_FILE)
+       $(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \
+               $(LIBS) $(OPENSSL_LIBSSL)
 
 init-db.o: init-db.c
        $(CC) -c $(ALL_CFLAGS) \
-               -DDEFAULT_GIT_TEMPLATE_DIR=$(call shellquote,"$(template_dir)") $*.c
+               -DDEFAULT_GIT_TEMPLATE_DIR='"$(template_dir_SQ)"' $*.c
 
 $(LIB_OBJS): $(LIB_H)
 $(patsubst git-%$X,%.o,$(PROGRAMS)): $(LIB_H)
@@ -499,6 +552,12 @@ doc:
 
 ### Testing rules
 
+# GNU make supports exporting all variables by "export" without parameters.
+# However, the environment gets quite big, and some programs have problems
+# with that.
+
+export NO_PYTHON
+
 test: all
        $(MAKE) -C t/ all
 
@@ -516,13 +575,13 @@ check:
 ### Installation rules
 
 install: all
-       $(INSTALL) -d -m755 $(call shellquote,$(DESTDIR)$(bindir))
-       $(INSTALL) -d -m755 $(call shellquote,$(DESTDIR)$(gitexecdir))
-       $(INSTALL) $(ALL_PROGRAMS) $(call shellquote,$(DESTDIR)$(gitexecdir))
-       $(INSTALL) git$X gitk $(call shellquote,$(DESTDIR)$(bindir))
+       $(INSTALL) -d -m755 '$(DESTDIR_SQ)$(bindir_SQ)'
+       $(INSTALL) -d -m755 '$(DESTDIR_SQ)$(gitexecdir_SQ)'
+       $(INSTALL) $(ALL_PROGRAMS) '$(DESTDIR_SQ)$(gitexecdir_SQ)'
+       $(INSTALL) git$X gitk '$(DESTDIR_SQ)$(bindir_SQ)'
        $(MAKE) -C templates install
-       $(INSTALL) -d -m755 $(call shellquote,$(DESTDIR)$(GIT_PYTHON_DIR))
-       $(INSTALL) $(PYMODULES) $(call shellquote,$(DESTDIR)$(GIT_PYTHON_DIR))
+       $(INSTALL) -d -m755 '$(DESTDIR_SQ)$(GIT_PYTHON_DIR_SQ)'
+       $(INSTALL) $(PYMODULES) '$(DESTDIR_SQ)$(GIT_PYTHON_DIR_SQ)'
 
 install-doc:
        $(MAKE) -C Documentation install
diff --git a/apply.c b/apply.c
index 453482a..9deb206 100644 (file)
--- a/apply.c
+++ b/apply.c
@@ -1232,6 +1232,14 @@ static int apply_one_fragment(struct buffer_desc *desc, struct fragment *frag)
                size -= len;
        }
 
+#ifdef NO_ACCURATE_DIFF
+       if (oldsize > 0 && old[oldsize - 1] == '\n' &&
+                       newsize > 0 && new[newsize - 1] == '\n') {
+               oldsize--;
+               newsize--;
+       }
+#endif
+                       
        offset = find_offset(buf, desc->size, old, oldsize, frag->newpos);
        if (offset >= 0) {
                int diff = newsize - oldsize;
@@ -1399,7 +1407,7 @@ static int check_patch(struct patch *patch)
                                        return -1;
                        }
 
-                       changed = ce_match_stat(active_cache[pos], &st);
+                       changed = ce_match_stat(active_cache[pos], &st, 1);
                        if (changed)
                                return error("%s: does not match index",
                                             old_name);
diff --git a/cache.h b/cache.h
index f686e72..0d3b244 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -10,7 +10,7 @@
 #define deflateBound(c,s)  ((s) + (((s) + 7) >> 3) + (((s) + 63) >> 6) + 11)
 #endif
 
-#if defined(DT_UNKNOWN) && !NO_D_TYPE_IN_DIRENT
+#if defined(DT_UNKNOWN) && !defined(NO_D_TYPE_IN_DIRENT)
 #define DTYPE(de)      ((de)->d_type)
 #else
 #undef DT_UNKNOWN
@@ -91,6 +91,7 @@ struct cache_entry {
 #define CE_NAMEMASK  (0x0fff)
 #define CE_STAGEMASK (0x3000)
 #define CE_UPDATE    (0x4000)
+#define CE_VALID     (0x8000)
 #define CE_STAGESHIFT 12
 
 #define create_ce_flags(len, stage) htons((len) | ((stage) << CE_STAGESHIFT))
@@ -144,8 +145,8 @@ extern int add_cache_entry(struct cache_entry *ce, int option);
 extern int remove_cache_entry_at(int pos);
 extern int remove_file_from_cache(const char *path);
 extern int ce_same_name(struct cache_entry *a, struct cache_entry *b);
-extern int ce_match_stat(struct cache_entry *ce, struct stat *st);
-extern int ce_modified(struct cache_entry *ce, struct stat *st);
+extern int ce_match_stat(struct cache_entry *ce, struct stat *st, int);
+extern int ce_modified(struct cache_entry *ce, struct stat *st, int);
 extern int ce_path_match(const struct cache_entry *ce, const char **pathspec);
 extern int index_fd(unsigned char *sha1, int fd, struct stat *st, int write_object, const char *type);
 extern int index_pipe(unsigned char *sha1, int fd, const char *type, int write_object);
@@ -162,6 +163,7 @@ extern void rollback_index_file(struct cache_file *);
 
 /* Environment bits from configuration mechanism */
 extern int trust_executable_bit;
+extern int assume_unchanged;
 extern int only_use_symrefs;
 extern int diff_rename_limit_default;
 extern int shared_repository;
@@ -324,7 +326,7 @@ extern int num_packed_objects(const struct packed_git *p);
 extern int nth_packed_object_sha1(const struct packed_git *, int, unsigned char*);
 extern int find_pack_entry_one(const unsigned char *, struct pack_entry *, struct packed_git *);
 extern void *unpack_entry_gently(struct pack_entry *, char *, unsigned long *);
-extern void packed_object_info_detail(struct pack_entry *, char *, unsigned long *, unsigned long *, int *, unsigned char *);
+extern void packed_object_info_detail(struct pack_entry *, char *, unsigned long *, unsigned long *, unsigned int *, unsigned char *);
 
 /* Dumb servers support */
 extern int update_server_info(int);
index 96d66b4..1a613f3 100644 (file)
@@ -4,6 +4,92 @@
  * Copyright (C) Linus Torvalds, 2005
  */
 #include "cache.h"
+#include "exec_cmd.h"
+
+static void flush_buffer(const char *buf, unsigned long size)
+{
+       while (size > 0) {
+               long ret = xwrite(1, buf, size);
+               if (ret < 0) {
+                       /* Ignore epipe */
+                       if (errno == EPIPE)
+                               break;
+                       die("git-cat-file: %s", strerror(errno));
+               } else if (!ret) {
+                       die("git-cat-file: disk full?");
+               }
+               size -= ret;
+               buf += ret;
+       }
+}
+
+static int pprint_tag(const unsigned char *sha1, const char *buf, unsigned long size)
+{
+       /* the parser in tag.c is useless here. */
+       const char *endp = buf + size;
+       const char *cp = buf;
+
+       while (cp < endp) {
+               char c = *cp++;
+               if (c != '\n')
+                       continue;
+               if (7 <= endp - cp && !memcmp("tagger ", cp, 7)) {
+                       const char *tagger = cp;
+
+                       /* Found the tagger line.  Copy out the contents
+                        * of the buffer so far.
+                        */
+                       flush_buffer(buf, cp - buf);
+
+                       /*
+                        * Do something intelligent, like pretty-printing
+                        * the date.
+                        */
+                       while (cp < endp) {
+                               if (*cp++ == '\n') {
+                                       /* tagger to cp is a line
+                                        * that has ident and time.
+                                        */
+                                       const char *sp = tagger;
+                                       char *ep;
+                                       unsigned long date;
+                                       long tz;
+                                       while (sp < cp && *sp != '>')
+                                               sp++;
+                                       if (sp == cp) {
+                                               /* give up */
+                                               flush_buffer(tagger,
+                                                            cp - tagger);
+                                               break;
+                                       }
+                                       while (sp < cp &&
+                                              !('0' <= *sp && *sp <= '9'))
+                                               sp++;
+                                       flush_buffer(tagger, sp - tagger);
+                                       date = strtoul(sp, &ep, 10);
+                                       tz = strtol(ep, NULL, 10);
+                                       sp = show_date(date, tz);
+                                       flush_buffer(sp, strlen(sp));
+                                       xwrite(1, "\n", 1);
+                                       break;
+                               }
+                       }
+                       break;
+               }
+               if (cp < endp && *cp == '\n')
+                       /* end of header */
+                       break;
+       }
+       /* At this point, we have copied out the header up to the end of
+        * the tagger line and cp points at one past \n.  It could be the
+        * next header line after the tagger line, or it could be another
+        * \n that marks the end of the headers.  We need to copy out the
+        * remainder as is.
+        */
+       if (cp < endp)
+               flush_buffer(cp, endp - cp);
+       return 0;
+}
 
 int main(int argc, char **argv)
 {
@@ -15,7 +101,7 @@ int main(int argc, char **argv)
 
        setup_git_directory();
        if (argc != 3 || get_sha1(argv[2], sha1))
-               usage("git-cat-file [-t|-s|-e|<type>] <sha1>");
+               usage("git-cat-file [-t|-s|-e|-p|<type>] <sha1>");
 
        opt = 0;
        if ( argv[1][0] == '-' ) {
@@ -43,6 +129,23 @@ int main(int argc, char **argv)
        case 'e':
                return !has_sha1_file(sha1);
 
+       case 'p':
+               if (get_sha1(argv[2], sha1) ||
+                   sha1_object_info(sha1, type, NULL))
+                       die("Not a valid object name %s", argv[2]);
+
+               /* custom pretty-print here */
+               if (!strcmp(type, "tree"))
+                       return execl_git_cmd("ls-tree", argv[2], NULL);
+
+               buf = read_sha1_file(sha1, type, &size);
+               if (!buf)
+                       die("Cannot read object %s", argv[2]);
+               if (!strcmp(type, "tag"))
+                       return pprint_tag(sha1, buf, size);
+
+               /* otherwise just spit out the data */
+               break;
        case 0:
                buf = read_object_with_reference(sha1, argv[1], &size, NULL);
                break;
@@ -54,18 +157,6 @@ int main(int argc, char **argv)
        if (!buf)
                die("git-cat-file %s: bad file", argv[2]);
 
-       while (size > 0) {
-               long ret = xwrite(1, buf, size);
-               if (ret < 0) {
-                       /* Ignore epipe */
-                       if (errno == EPIPE)
-                               break;
-                       die("git-cat-file: %s", strerror(errno));
-               } else if (!ret) {
-                       die("git-cat-file: disk full?");
-               }
-               size -= ret;
-               buf += ret;
-       }
+       flush_buffer(buf, size);
        return 0;
 }
index 53dd8cb..957b4a8 100644 (file)
@@ -116,6 +116,7 @@ int main(int argc, char **argv)
        int all = 0;
 
        prefix = setup_git_directory();
+       git_config(git_default_config);
        prefix_length = prefix ? strlen(prefix) : 0;
 
        if (read_cache() < 0) {
index 512b5d7..06d5439 100644 (file)
--- a/commit.c
+++ b/commit.c
@@ -572,7 +572,7 @@ int count_parents(struct commit * commit)
 /*
  * Performs an in-place topological sort on the list supplied.
  */
-void sort_in_topological_order(struct commit_list ** list)
+void sort_in_topological_order(struct commit_list ** list, int lifo)
 {
        struct commit_list * next = *list;
        struct commit_list * work = NULL, **insert;
@@ -631,7 +631,10 @@ void sort_in_topological_order(struct commit_list ** list)
                }
                next=next->next;
        }
+
        /* process the list in topological order */
+       if (!lifo)
+               sort_by_date(&work);
        while (work) {
                struct commit * work_item = pop_commit(&work);
                struct sort_node * work_node = (struct sort_node *)work_item->object.util;
@@ -648,8 +651,12 @@ void sort_in_topological_order(struct commit_list ** list)
                                  * guaranteeing topological order.
                                  */
                                pn->indegree--;
-                               if (!pn->indegree) 
-                                       commit_list_insert(parent, &work);
+                               if (!pn->indegree) {
+                                       if (!lifo)
+                                               insert_by_date(parent, &work);
+                                       else
+                                               commit_list_insert(parent, &work);
+                               }
                        }
                        parents=parents->next;
                }
index 986b22d..70a7c75 100644 (file)
--- a/commit.h
+++ b/commit.h
@@ -72,6 +72,8 @@ int count_parents(struct commit * commit);
  * Post-conditions: 
  *   invariant of resulting list is:
  *      a reachable from b => ord(b) < ord(a)
+ *   in addition, when lifo == 0, commits on parallel tracks are
+ *   sorted in the dates order.
  */
-void sort_in_topological_order(struct commit_list ** list);
+void sort_in_topological_order(struct commit_list ** list, int lifo);
 #endif /* COMMIT_H */
index 8355224..7dbdce1 100644 (file)
--- a/config.c
+++ b/config.c
@@ -222,6 +222,11 @@ int git_default_config(const char *var, const char *value)
                return 0;
        }
 
+       if (!strcmp(var, "core.ignorestat")) {
+               assume_unchanged = git_config_bool(var, value);
+               return 0;
+       }
+
        if (!strcmp(var, "core.symrefsonly")) {
                only_use_symrefs = git_config_bool(var, value);
                return 0;
diff --git a/contrib/README b/contrib/README
new file mode 100644 (file)
index 0000000..e1c0a01
--- /dev/null
@@ -0,0 +1,44 @@
+Contributed Software
+
+Although these pieces are available as part of the official git
+source tree, they are in somewhat different status.  The
+intention is to keep interesting tools around git here, maybe
+even experimental ones, to give users an easier access to them,
+and to give tools wider exposure, so that they can be improved
+faster.
+
+I am not expecting to touch these myself that much.  As far as
+my day-to-day operation is concerned, these subdirectories are
+owned by their respective primary authors.  I am willing to help
+if users of these components and the contrib/ subtree "owners"
+have technical/design issues to resolve, but the initiative to
+fix and/or enhance things _must_ be on the side of the subtree
+owners.  IOW, I won't be actively looking for bugs and rooms for
+enhancements in them as the git maintainer -- I may only do so
+just as one of the users when I want to scratch my own itch.  If
+you have patches to things in contrib/ area, the patch should be
+first sent to the primary author, and then the primary author
+should ack and forward it to me (git pull request is nicer).
+This is the same way as how I have been treating gitk, and to a
+lesser degree various foreign SCM interfaces, so you know the
+drill.
+
+I expect that things that start their life in the contrib/ area
+to graduate out of contrib/ once they mature, either by becoming
+projects on their own, or moving to the toplevel directory.  On
+the other hand, I expect I'll be proposing removal of disused
+and inactive ones from time to time.
+
+If you have new things to add to this area, please first propose
+it on the git mailing list, and after a list discussion proves
+there are some general interests (it does not have to be a
+list-wide consensus for a tool targeted to a relatively narrow
+audience -- for example I do not work with projects whose
+upstream is svn, so I have no use for git-svn myself, but it is
+of general interest for people who need to interoperate with SVN
+repositories in a way git-svn works better than git-svnimport),
+submit a patch to create a subdirectory of contrib/ and put your
+stuff there.
+
+-jc
+
diff --git a/contrib/emacs/git.el b/contrib/emacs/git.el
new file mode 100644 (file)
index 0000000..8f23477
--- /dev/null
@@ -0,0 +1,956 @@
+;;; git.el --- A user interface for git
+
+;; Copyright (C) 2005, 2006 Alexandre Julliard <julliard@winehq.org>
+
+;; Version: 1.0
+
+;; This program is free software; you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License as
+;; published by the Free Software Foundation; either version 2 of
+;; the License, or (at your option) any later version.
+;;
+;; This program is distributed in the hope that it will be
+;; useful, but WITHOUT ANY WARRANTY; without even the implied
+;; warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+;; PURPOSE.  See the GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public
+;; License along with this program; if not, write to the Free
+;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+;; MA 02111-1307 USA
+
+;;; Commentary:
+
+;; This file contains an interface for the git version control
+;; system. It provides easy access to the most frequently used git
+;; commands. The user interface is as far as possible identical to
+;; that of the PCL-CVS mode.
+;;
+;; To install: put this file on the load-path and place the following
+;; in your .emacs file:
+;;
+;;    (require 'git)
+;;
+;; To start: `M-x git-status'
+;;
+;; TODO
+;;  - portability to XEmacs
+;;  - better handling of subprocess errors
+;;  - hook into file save (after-save-hook)
+;;  - diff against other branch
+;;  - renaming files from the status buffer
+;;  - support for appending signed-off-by
+;;  - creating tags
+;;  - fetch/pull
+;;  - switching branches
+;;  - revlist browser
+;;  - git-show-branch browser
+;;  - customize support
+;;  - menus
+;;
+
+(eval-when-compile (require 'cl))
+(require 'ewoc)
+
+
+;;;; Faces
+;;;; ------------------------------------------------------------
+
+(defface git-status-face
+  '((((class color) (background light)) (:foreground "purple")))
+  "Git mode face used to highlight added and modified files.")
+
+(defface git-unmerged-face
+  '((((class color) (background light)) (:foreground "red" :bold t)))
+  "Git mode face used to highlight unmerged files.")
+
+(defface git-unknown-face
+  '((((class color) (background light)) (:foreground "goldenrod" :bold t)))
+  "Git mode face used to highlight unknown files.")
+
+(defface git-uptodate-face
+  '((((class color) (background light)) (:foreground "grey60")))
+  "Git mode face used to highlight up-to-date files.")
+
+(defface git-ignored-face
+  '((((class color) (background light)) (:foreground "grey60")))
+  "Git mode face used to highlight ignored files.")
+
+(defface git-mark-face
+  '((((class color) (background light)) (:foreground "red" :bold t)))
+  "Git mode face used for the file marks.")
+
+(defface git-header-face
+  '((((class color) (background light)) (:foreground "blue")))
+  "Git mode face used for commit headers.")
+
+(defface git-separator-face
+  '((((class color) (background light)) (:foreground "brown")))
+  "Git mode face used for commit separator.")
+
+(defface git-permission-face
+  '((((class color) (background light)) (:foreground "green" :bold t)))
+  "Git mode face used for permission changes.")
+
+(defvar git-committer-name nil
+  "*User name to use for commits.
+If not set, fall back to `add-log-full-name' and then `user-full-name'.")
+
+(defvar git-committer-email nil
+  "*Email address to use for commits.
+If not set, fall back to `add-log-mailing-address' and then `user-mail-address'.")
+
+(defvar git-commits-coding-system 'utf-8
+  "Default coding system for git commits.")
+
+(defconst git-log-msg-separator "--- log message follows this line ---")
+
+(defconst git-per-dir-ignore-file ".gitignore"
+  "Name of the per-directory ignore file.")
+
+
+;;;; Utilities
+;;;; ------------------------------------------------------------
+
+(defun git-get-env-strings (env)
+  "Build a list of NAME=VALUE strings from a list of environment strings."
+  (mapcar (lambda (entry) (concat (car entry) "=" (cdr entry))) env))
+
+(defun git-call-process-env (buffer env &rest args)
+  "Wrapper for call-process that sets environment strings."
+  (if env
+      (apply #'call-process "env" nil buffer nil
+             (append (git-get-env-strings env) (list "git") args))
+    (apply #'call-process "git" nil buffer nil args)))
+
+(defun git-run-process-region (buffer start end program args)
+  "Run a git process with a buffer region as input."
+  (let ((output-buffer (current-buffer))
+        (dir default-directory))
+    (with-current-buffer buffer
+      (cd dir)
+      (apply #'call-process-region start end program
+             nil (list output-buffer nil) nil args))))
+
+(defun git-run-command-buffer (buffer-name &rest args)
+  "Run a git command, sending the output to a buffer named BUFFER-NAME."
+  (let ((dir default-directory)
+        (buffer (get-buffer-create buffer-name)))
+    (message "Running git %s..." (car args))
+    (with-current-buffer buffer
+      (let ((default-directory dir)
+            (buffer-read-only nil))
+        (erase-buffer)
+        (apply #'git-call-process-env buffer nil args)))
+    (message "Running git %s...done" (car args))
+    buffer))
+
+(defun git-run-command (buffer env &rest args)
+  (message "Running git %s..." (car args))
+  (apply #'git-call-process-env buffer env args)
+  (message "Running git %s...done" (car args)))
+
+(defun git-run-command-region (buffer start end env &rest args)
+  "Run a git command with specified buffer region as input."
+  (message "Running git %s..." (car args))
+  (unless (eq 0 (if env
+                    (git-run-process-region
+                     buffer start end "env"
+                     (append (git-get-env-strings env) (list "git") args))
+                  (git-run-process-region
+                   buffer start end "git" args)))
+    (error "Failed to run \"git %s\":\n%s" (mapconcat (lambda (x) x) args " ") (buffer-string)))
+  (message "Running git %s...done" (car args)))
+
+(defun git-get-string-sha1 (string)
+  "Read a SHA1 from the specified string."
+  (let ((pos (string-match "[0-9a-f]\\{40\\}" string)))
+    (and pos (substring string pos (match-end 0)))))
+
+(defun git-get-committer-name ()
+  "Return the name to use as GIT_COMMITTER_NAME."
+  ; copied from log-edit
+  (or git-committer-name
+      (and (boundp 'add-log-full-name) add-log-full-name)
+      (and (fboundp 'user-full-name) (user-full-name))
+      (and (boundp 'user-full-name) user-full-name)))
+
+(defun git-get-committer-email ()
+  "Return the email address to use as GIT_COMMITTER_EMAIL."
+  ; copied from log-edit
+  (or git-committer-email
+      (and (boundp 'add-log-mailing-address) add-log-mailing-address)
+      (and (fboundp 'user-mail-address) (user-mail-address))
+      (and (boundp 'user-mail-address) user-mail-address)))
+
+(defun git-escape-file-name (name)
+  "Escape a file name if necessary."
+  (if (string-match "[\n\t\"\\]" name)
+      (concat "\""
+              (mapconcat (lambda (c)
+                   (case c
+                     (?\n "\\n")
+                     (?\t "\\t")
+                     (?\\ "\\\\")
+                     (?\" "\\\"")
+                     (t (char-to-string c))))
+                 name "")
+              "\"")
+    name))
+
+(defun git-get-top-dir (dir)
+  "Retrieve the top-level directory of a git tree."
+  (let ((cdup (with-output-to-string
+                (with-current-buffer standard-output
+                  (cd dir)
+                  (unless (eq 0 (call-process "git" nil t nil "rev-parse" "--show-cdup"))
+                    (error "cannot find top-level git tree for %s." dir))))))
+    (expand-file-name (concat (file-name-as-directory dir)
+                              (car (split-string cdup "\n"))))))
+
+;stolen from pcl-cvs
+(defun git-append-to-ignore (file)
+  "Add a file name to the ignore file in its directory."
+  (let* ((fullname (expand-file-name file))
+         (dir (file-name-directory fullname))
+         (name (file-name-nondirectory fullname)))
+  (save-window-excursion
+    (set-buffer (find-file-noselect (expand-file-name git-per-dir-ignore-file dir)))
+    (goto-char (point-max))
+    (unless (zerop (current-column)) (insert "\n"))
+    (insert name "\n")
+    (sort-lines nil (point-min) (point-max))
+    (save-buffer))))
+
+
+;;;; Wrappers for basic git commands
+;;;; ------------------------------------------------------------
+
+(defun git-rev-parse (rev)
+  "Parse a revision name and return its SHA1."
+  (git-get-string-sha1
+   (with-output-to-string
+     (with-current-buffer standard-output
+       (git-call-process-env t nil "rev-parse" rev)))))
+
+(defun git-symbolic-ref (ref)
+  "Wrapper for the git-symbolic-ref command."
+  (car
+   (split-string
+    (with-output-to-string
+      (with-current-buffer standard-output
+        (git-call-process-env t nil "symbolic-ref" ref)))
+    "\n")))
+
+(defun git-update-ref (ref val &optional oldval)
+  "Update a reference by calling git-update-ref."
+  (apply #'git-call-process-env nil nil "update-ref" ref val (if oldval (list oldval))))
+
+(defun git-read-tree (tree &optional index-file)
+  "Read a tree into the index file."
+  (apply #'git-call-process-env nil
+         (if index-file `(("GIT_INDEX_FILE" . ,index-file)) nil)
+         "read-tree" (if tree (list tree))))
+
+(defun git-write-tree (&optional index-file)
+  "Call git-write-tree and return the resulting tree SHA1 as a string."
+  (git-get-string-sha1
+   (with-output-to-string
+     (with-current-buffer standard-output
+       (git-call-process-env t
+        (if index-file `(("GIT_INDEX_FILE" . ,index-file)) nil)
+        "write-tree")))))
+
+(defun git-commit-tree (buffer tree head)
+  "Call git-commit-tree with buffer as input and return the resulting commit SHA1."
+  (let ((author-name (git-get-committer-name))
+        (author-email (git-get-committer-email))
+        author-date log-start log-end args)
+    (when head
+      (push "-p" args)
+      (push head args))
+    (with-current-buffer buffer
+      (goto-char (point-min))
+      (if
+          (setq log-start (re-search-forward (concat "^" git-log-msg-separator "\n") nil t))
+          (save-restriction
+            (narrow-to-region (point-min) log-start)
+            (goto-char (point-min))
+            (when (re-search-forward "^Author: +\\(.*?\\) *<\\(.*\\)> *$" nil t)
+              (setq author-name (match-string 1)
+                    author-email (match-string 2)))
+            (goto-char (point-min))
+            (when (re-search-forward "^Date: +\\(.*\\)$" nil t)
+              (setq author-date (match-string 1)))
+            (goto-char (point-min))
+            (while (re-search-forward "^Parent: +\\([0-9a-f]+\\)" nil t)
+              (unless (string-equal head (match-string 1))
+                (push "-p" args)
+                (push (match-string 1) args))))
+        (setq log-start (point-min)))
+      (setq log-end (point-max)))
+    (git-get-string-sha1
+     (with-output-to-string
+       (with-current-buffer standard-output
+         (let ((coding-system-for-write git-commits-coding-system)
+               (env `(("GIT_AUTHOR_NAME" . ,author-name)
+                      ("GIT_AUTHOR_EMAIL" . ,author-email)
+                      ("GIT_COMMITTER_NAME" . ,(git-get-committer-name))
+                      ("GIT_COMMITTER_EMAIL" . ,(git-get-committer-email)))))
+           (when author-date (push `("GIT_AUTHOR_DATE" . ,author-date) env))
+           (apply #'git-run-command-region
+                  buffer log-start log-end env
+                  "commit-tree" tree (nreverse args))))))))
+
+(defun git-empty-db-p ()
+  "Check if the git db is empty (no commit done yet)."
+  (not (eq 0 (call-process "git" nil nil nil "rev-parse" "--verify" "HEAD"))))
+
+(defun git-get-merge-heads ()
+  "Retrieve the merge heads from the MERGE_HEAD file if present."
+  (let (heads)
+    (when (file-readable-p ".git/MERGE_HEAD")
+      (with-temp-buffer
+        (insert-file-contents ".git/MERGE_HEAD" nil nil nil t)
+        (goto-char (point-min))
+        (while (re-search-forward "[0-9a-f]\\{40\\}" nil t)
+          (push (match-string 0) heads))))
+    (nreverse heads)))
+
+;;;; File info structure
+;;;; ------------------------------------------------------------
+
+; fileinfo structure stolen from pcl-cvs
+(defstruct (git-fileinfo
+            (:copier nil)
+            (:constructor git-create-fileinfo (state name &optional old-perm new-perm rename-state orig-name marked))
+            (:conc-name git-fileinfo->))
+  marked              ;; t/nil
+  state               ;; current state
+  name                ;; file name
+  old-perm new-perm   ;; permission flags
+  rename-state        ;; rename or copy state
+  orig-name           ;; original name for renames or copies
+  needs-refresh)      ;; whether file needs to be refreshed
+
+(defvar git-status nil)
+
+(defun git-clear-status (status)
+  "Remove everything from the status list."
+  (ewoc-filter status (lambda (info) nil)))
+
+(defun git-set-files-state (files state)
+  "Set the state of a list of files."
+  (dolist (info files)
+    (unless (eq (git-fileinfo->state info) state)
+      (setf (git-fileinfo->state info) state)
+      (setf (git-fileinfo->rename-state info) nil)
+      (setf (git-fileinfo->orig-name info) nil)
+      (setf (git-fileinfo->needs-refresh info) t))))
+
+(defun git-state-code (code)
+  "Convert from a string to a added/deleted/modified state."
+  (case (string-to-char code)
+    (?M 'modified)
+    (?? 'unknown)
+    (?A 'added)
+    (?D 'deleted)
+    (?U 'unmerged)
+    (t nil)))
+
+(defun git-status-code-as-string (code)
+  "Format a git status code as string."
+  (case code
+    ('modified (propertize "Modified" 'face 'git-status-face))
+    ('unknown  (propertize "Unknown " 'face 'git-unknown-face))
+    ('added    (propertize "Added   " 'face 'git-status-face))
+    ('deleted  (propertize "Deleted " 'face 'git-status-face))
+    ('unmerged (propertize "Unmerged" 'face 'git-unmerged-face))
+    ('uptodate (propertize "Uptodate" 'face 'git-uptodate-face))
+    ('ignored  (propertize "Ignored " 'face 'git-ignored-face))
+    (t "?       ")))
+
+(defun git-rename-as-string (info)
+  "Return a string describing the copy or rename associated with INFO, or an empty string if none."
+  (let ((state (git-fileinfo->rename-state info)))
+    (if state
+        (propertize
+         (concat "   ("
+                 (if (eq state 'copy) "copied from "
+                   (if (eq (git-fileinfo->state info) 'added) "renamed to "
+                     "renamed from "))
+                 (git-escape-file-name (git-fileinfo->orig-name info))
+                 ")") 'face 'git-status-face)
+      "")))
+
+(defun git-permissions-as-string (old-perm new-perm)
+  "Format a permission change as string."
+  (propertize
+   (if (or (not old-perm)
+           (not new-perm)
+           (eq 0 (logand #O111 (logxor old-perm new-perm))))
+       "  "
+     (if (eq 0 (logand #O111 old-perm)) "+x" "-x"))
+  'face 'git-permission-face))
+
+(defun git-fileinfo-prettyprint (info)
+  "Pretty-printer for the git-fileinfo structure."
+  (insert (format "   %s %s %s  %s%s"
+                  (if (git-fileinfo->marked info) (propertize "*" 'face 'git-mark-face) " ")
+                  (git-status-code-as-string (git-fileinfo->state info))
+                  (git-permissions-as-string (git-fileinfo->old-perm info) (git-fileinfo->new-perm info))
+                  (git-escape-file-name (git-fileinfo->name info))
+                  (git-rename-as-string info))))
+
+(defun git-parse-status (status)
+  "Parse the output of git-diff-index in the current buffer."
+  (goto-char (point-min))
+  (while (re-search-forward
+          ":\\([0-7]\\{6\\}\\) \\([0-7]\\{6\\}\\) [0-9a-f]\\{40\\} [0-9a-f]\\{40\\} \\(\\([ADMU]\\)\0\\([^\0]+\\)\\|\\([CR]\\)[0-9]*\0\\([^\0]+\\)\0\\([^\0]+\\)\\)\0"
+          nil t 1)
+    (let ((old-perm (string-to-number (match-string 1) 8))
+          (new-perm (string-to-number (match-string 2) 8))
+          (state (or (match-string 4) (match-string 6)))
+          (name (or (match-string 5) (match-string 7)))
+          (new-name (match-string 8)))
+      (if new-name  ; copy or rename
+          (if (eq ?C (string-to-char state))
+              (ewoc-enter-last status (git-create-fileinfo 'added new-name old-perm new-perm 'copy name))
+            (ewoc-enter-last status (git-create-fileinfo 'deleted name 0 0 'rename new-name))
+            (ewoc-enter-last status (git-create-fileinfo 'added new-name old-perm new-perm 'rename name)))
+        (ewoc-enter-last status (git-create-fileinfo (git-state-code state) name old-perm new-perm))))))
+
+(defun git-find-status-file (status file)
+  "Find a given file in the status ewoc and return its node."
+  (let ((node (ewoc-nth status 0)))
+    (while (and node (not (string= file (git-fileinfo->name (ewoc-data node)))))
+      (setq node (ewoc-next status node)))
+    node))
+
+(defun git-parse-ls-files (status default-state &optional skip-existing)
+  "Parse the output of git-ls-files in the current buffer."
+  (goto-char (point-min))
+  (let (infolist)
+    (while (re-search-forward "\\([HMRCK?]\\) \\([^\0]*\\)\0" nil t 1)
+      (let ((state (match-string 1))
+            (name (match-string 2)))
+        (unless (and skip-existing (git-find-status-file status name))
+          (push (git-create-fileinfo (or (git-state-code state) default-state) name) infolist))))
+    (dolist (info (nreverse infolist))
+      (ewoc-enter-last status info))))
+
+(defun git-parse-ls-unmerged (status)
+  "Parse the output of git-ls-files -u in the current buffer."
+  (goto-char (point-min))
+  (let (files)
+    (while (re-search-forward "[0-7]\\{6\\} [0-9a-f]\\{40\\} [123]\t\\([^\0]+\\)\0" nil t)
+      (let ((node (git-find-status-file status (match-string 1))))
+        (when node (push (ewoc-data node) files))))
+    (git-set-files-state files 'unmerged)))
+
+(defun git-add-status-file (state name)
+  "Add a new file to the status list (if not existing already) and return its node."
+  (unless git-status (error "Not in git-status buffer."))
+  (or (git-find-status-file git-status name)
+      (ewoc-enter-last git-status (git-create-fileinfo state name))))
+
+(defun git-marked-files ()
+  "Return a list of all marked files, or if none a list containing just the file at cursor position."
+  (unless git-status (error "Not in git-status buffer."))
+  (or (ewoc-collect git-status (lambda (info) (git-fileinfo->marked info)))
+      (list (ewoc-data (ewoc-locate git-status)))))
+
+(defun git-marked-files-state (&rest states)
+  "Return marked files that are in the specified states."
+  (let ((files (git-marked-files))
+        result)
+    (dolist (info files)
+      (when (memq (git-fileinfo->state info) states)
+        (push info result)))
+    result))
+
+(defun git-refresh-files ()
+  "Refresh all files that need it and clear the needs-refresh flag."
+  (unless git-status (error "Not in git-status buffer."))
+  (ewoc-map
+   (lambda (info)
+     (let ((refresh (git-fileinfo->needs-refresh info)))
+       (setf (git-fileinfo->needs-refresh info) nil)
+       refresh))
+   git-status)
+  ; move back to goal column
+  (when goal-column (move-to-column goal-column)))
+
+(defun git-refresh-ewoc-hf (status)
+  "Refresh the ewoc header and footer."
+  (let ((branch (git-symbolic-ref "HEAD"))
+        (head (if (git-empty-db-p) "Nothing committed yet"
+                (substring (git-rev-parse "HEAD") 0 10)))
+        (merge-heads (git-get-merge-heads)))
+    (ewoc-set-hf status
+                 (format "Directory:  %s\nBranch:     %s\nHead:       %s%s\n"
+                         default-directory
+                         (if (string-match "^refs/heads/" branch)
+                             (substring branch (match-end 0))
+                           branch)
+                         head
+                         (if merge-heads
+                             (concat "\nMerging:    "
+                                     (mapconcat (lambda (str) (substring str 0 10)) merge-heads " "))
+                           ""))
+                 (if (ewoc-nth status 0) "" "    No changes."))))
+
+(defun git-get-filenames (files)
+  (mapcar (lambda (info) (git-fileinfo->name info)) files))
+
+(defun git-update-index (index-file files)
+  "Run git-update-index on a list of files."
+  (let ((env (and index-file `(("GIT_INDEX_FILE" . ,index-file))))
+        added deleted modified)
+    (dolist (info files)
+      (case (git-fileinfo->state info)
+        ('added (push info added))
+        ('deleted (push info deleted))
+        ('modified (push info modified))))
+    (when added
+      (apply #'git-run-command nil env "update-index" "--add" "--" (git-get-filenames added)))
+    (when deleted
+      (apply #'git-run-command nil env "update-index" "--remove" "--" (git-get-filenames deleted)))
+    (when modified
+      (apply #'git-run-command nil env "update-index" "--" (git-get-filenames modified)))))
+
+(defun git-do-commit ()
+  "Perform the actual commit using the current buffer as log message."
+  (interactive)
+  (let ((buffer (current-buffer))
+        (index-file (make-temp-file "gitidx")))
+    (with-current-buffer log-edit-parent-buffer
+      (if (git-marked-files-state 'unmerged)
+          (message "You cannot commit unmerged files, resolve them first.")
+        (unwind-protect
+            (let ((files (git-marked-files-state 'added 'deleted 'modified))
+                  head head-tree)
+              (unless (git-empty-db-p)
+                (setq head (git-rev-parse "HEAD")
+                      head-tree (git-rev-parse "HEAD^{tree}")))
+              (if files
+                  (progn
+                    (git-read-tree head-tree index-file)
+                    (git-update-index nil files)         ;update both the default index
+                    (git-update-index index-file files)  ;and the temporary one
+                    (let ((tree (git-write-tree index-file)))
+                      (if (or (not (string-equal tree head-tree))
+                              (yes-or-no-p "The tree was not modified, do you really want to perform an empty commit? "))
+                          (let ((commit (git-commit-tree buffer tree head)))
+                            (git-update-ref "HEAD" commit head)
+                            (condition-case nil (delete-file ".git/MERGE_HEAD") (error nil))
+                            (with-current-buffer buffer (erase-buffer))
+                            (git-set-files-state files 'uptodate)
+                            (git-refresh-files)
+                            (git-refresh-ewoc-hf git-status)
+                            (message "Committed %s." commit))
+                        (message "Commit aborted."))))
+                (message "No files to commit.")))
+          (delete-file index-file))))))
+
+
+;;;; Interactive functions
+;;;; ------------------------------------------------------------
+
+(defun git-mark-file ()
+  "Mark the file that the cursor is on and move to the next one."
+  (interactive)
+  (unless git-status (error "Not in git-status buffer."))
+  (let* ((pos (ewoc-locate git-status))
+         (info (ewoc-data pos)))
+    (setf (git-fileinfo->marked info) t)
+    (ewoc-invalidate git-status pos)
+    (ewoc-goto-next git-status 1)))
+
+(defun git-unmark-file ()
+  "Unmark the file that the cursor is on and move to the next one."
+  (interactive)
+  (unless git-status (error "Not in git-status buffer."))
+  (let* ((pos (ewoc-locate git-status))
+         (info (ewoc-data pos)))
+    (setf (git-fileinfo->marked info) nil)
+    (ewoc-invalidate git-status pos)
+    (ewoc-goto-next git-status 1)))
+
+(defun git-unmark-file-up ()
+  "Unmark the file that the cursor is on and move to the previous one."
+  (interactive)
+  (unless git-status (error "Not in git-status buffer."))
+  (let* ((pos (ewoc-locate git-status))
+         (info (ewoc-data pos)))
+    (setf (git-fileinfo->marked info) nil)
+    (ewoc-invalidate git-status pos)
+    (ewoc-goto-prev git-status 1)))
+
+(defun git-mark-all ()
+  "Mark all files."
+  (interactive)
+  (unless git-status (error "Not in git-status buffer."))
+  (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) t) t) git-status)
+  ; move back to goal column after invalidate
+  (when goal-column (move-to-column goal-column)))
+
+(defun git-unmark-all ()
+  "Unmark all files."
+  (interactive)
+  (unless git-status (error "Not in git-status buffer."))
+  (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) nil) t) git-status)
+  ; move back to goal column after invalidate
+  (when goal-column (move-to-column goal-column)))
+
+(defun git-toggle-all-marks ()
+  "Toggle all file marks."
+  (interactive)
+  (unless git-status (error "Not in git-status buffer."))
+  (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) (not (git-fileinfo->marked info))) t) git-status)
+  ; move back to goal column after invalidate
+  (when goal-column (move-to-column goal-column)))
+
+(defun git-next-file (&optional n)
+  "Move the selection down N files."
+  (interactive "p")
+  (unless git-status (error "Not in git-status buffer."))
+  (ewoc-goto-next git-status n))
+
+(defun git-prev-file (&optional n)
+  "Move the selection up N files."
+  (interactive "p")
+  (unless git-status (error "Not in git-status buffer."))
+  (ewoc-goto-prev git-status n))
+
+(defun git-add-file ()
+  "Add marked file(s) to the index cache."
+  (interactive)
+  (let ((files (git-marked-files-state 'unknown)))
+    (unless files
+      (push (ewoc-data
+             (git-add-status-file 'added (file-relative-name
+                                          (read-file-name "File to add: " nil nil t))))
+            files))
+    (apply #'git-run-command nil nil "update-index" "--info-only" "--add" "--" (git-get-filenames files))
+    (git-set-files-state files 'added)
+    (git-refresh-files)))
+
+(defun git-ignore-file ()
+  "Add marked file(s) to the ignore list."
+  (interactive)
+  (let ((files (git-marked-files-state 'unknown)))
+    (unless files
+      (push (ewoc-data
+             (git-add-status-file 'unknown (file-relative-name
+                                            (read-file-name "File to ignore: " nil nil t))))
+            files))
+    (dolist (info files) (git-append-to-ignore (git-fileinfo->name info)))
+    (git-set-files-state files 'ignored)
+    (git-refresh-files)))
+
+(defun git-remove-file ()
+  "Remove the marked file(s)."
+  (interactive)
+  (let ((files (git-marked-files-state 'added 'modified 'unknown 'uptodate)))
+    (unless files
+      (push (ewoc-data
+             (git-add-status-file 'unknown (file-relative-name
+                                            (read-file-name "File to remove: " nil nil t))))
+            files))
+    (if (yes-or-no-p
+         (format "Remove %d file%s? " (length files) (if (> (length files) 1) "s" "")))
+        (progn
+          (dolist (info files)
+            (let ((name (git-fileinfo->name info)))
+              (when (file-exists-p name) (delete-file name))))
+          (apply #'git-run-command nil nil "update-index" "--info-only" "--remove" "--" (git-get-filenames files))
+          ; remove unknown files from the list, set the others to deleted
+          (ewoc-filter git-status
+                       (lambda (info files)
+                         (not (and (memq info files) (eq (git-fileinfo->state info) 'unknown))))
+                       files)
+          (git-set-files-state files 'deleted)
+          (git-refresh-files)
+          (unless (ewoc-nth git-status 0)  ; refresh header if list is empty
+            (git-refresh-ewoc-hf git-status)))
+      (message "Aborting"))))
+
+(defun git-revert-file ()
+  "Revert changes to the marked file(s)."
+  (interactive)
+  (let ((files (git-marked-files))
+        added modified)
+    (when (and files
+               (yes-or-no-p
+                (format "Revert %d file%s? " (length files) (if (> (length files) 1) "s" ""))))
+      (dolist (info files)
+        (case (git-fileinfo->state info)
+          ('added (push info added))
+          ('deleted (push info modified))
+          ('unmerged (push info modified))
+          ('modified (push info modified))))
+      (when added
+          (apply #'git-run-command nil nil "update-index" "--force-remove" "--" (git-get-filenames added))
+          (git-set-files-state added 'unknown))
+      (when modified
+          (apply #'git-run-command nil nil "checkout" "HEAD" (git-get-filenames modified))
+          (git-set-files-state modified 'uptodate))
+      (git-refresh-files))))
+
+(defun git-resolve-file ()
+  "Resolve conflicts in marked file(s)."
+  (interactive)
+  (let ((files (git-marked-files-state 'unmerged)))
+    (when files
+      (apply #'git-run-command nil nil "update-index" "--info-only" "--" (git-get-filenames files))
+      (git-set-files-state files 'modified)
+      (git-refresh-files))))
+
+(defun git-remove-handled ()
+  "Remove handled files from the status list."
+  (interactive)
+  (ewoc-filter git-status
+               (lambda (info)
+                 (not (or (eq (git-fileinfo->state info) 'ignored)
+                          (eq (git-fileinfo->state info) 'uptodate)))))
+  (unless (ewoc-nth git-status 0)  ; refresh header if list is empty
+    (git-refresh-ewoc-hf git-status)))
+
+(defun git-setup-diff-buffer (buffer)
+  "Setup a buffer for displaying a diff."
+  (with-current-buffer buffer
+    (diff-mode)
+    (goto-char (point-min))
+    (setq buffer-read-only t))
+  (display-buffer buffer)
+  (shrink-window-if-larger-than-buffer))
+
+(defun git-diff-file ()
+  "Diff the marked file(s) against HEAD."
+  (interactive)
+  (let ((files (git-marked-files)))
+    (git-setup-diff-buffer
+     (apply #'git-run-command-buffer "*git-diff*" "diff-index" "-p" "-M" "HEAD" "--" (git-get-filenames files)))))
+
+(defun git-diff-unmerged-file (stage)
+  "Diff the marked unmerged file(s) against the specified stage."
+  (let ((files (git-marked-files)))
+    (git-setup-diff-buffer
+     (apply #'git-run-command-buffer "*git-diff*" "diff-files" "-p" stage "--" (git-get-filenames files)))))
+
+(defun git-diff-file-base ()
+  "Diff the marked unmerged file(s) against the common base file."
+  (interactive)
+  (git-diff-unmerged-file "-1"))
+
+(defun git-diff-file-mine ()
+  "Diff the marked unmerged file(s) against my pre-merge version."
+  (interactive)
+  (git-diff-unmerged-file "-2"))
+
+(defun git-diff-file-other ()
+  "Diff the marked unmerged file(s) against the other's pre-merge version."
+  (interactive)
+  (git-diff-unmerged-file "-3"))
+
+(defun git-diff-file-combined ()
+  "Do a combined diff of the marked unmerged file(s)."
+  (interactive)
+  (git-diff-unmerged-file "-c"))
+
+(defun git-diff-file-idiff ()
+  "Perform an interactive diff on the current file."
+  (interactive)
+  (error "Interactive diffs not implemented yet."))
+
+(defun git-log-file ()
+  "Display a log of changes to the marked file(s)."
+  (interactive)
+  (let* ((files (git-marked-files))
+         (coding-system-for-read git-commits-coding-system)
+         (buffer (apply #'git-run-command-buffer "*git-log*" "rev-list" "--pretty" "HEAD" "--" (git-get-filenames files))))
+    (with-current-buffer buffer
+      ; (git-log-mode)  FIXME: implement log mode
+      (goto-char (point-min))
+      (setq buffer-read-only t))
+    (display-buffer buffer)))
+
+(defun git-log-edit-files ()
+  "Return a list of marked files for use in the log-edit buffer."
+  (with-current-buffer log-edit-parent-buffer
+    (git-get-filenames (git-marked-files-state 'added 'deleted 'modified))))
+
+(defun git-commit-file ()
+  "Commit the marked file(s), asking for a commit message."
+  (interactive)
+  (unless git-status (error "Not in git-status buffer."))
+  (let ((buffer (get-buffer-create "*git-commit*"))
+        (merge-heads (git-get-merge-heads))
+        (dir default-directory))
+    (with-current-buffer buffer
+      (when (eq 0 (buffer-size))
+        (cd dir)
+        (erase-buffer)
+        (insert
+         (propertize
+          (format "Author: %s <%s>\n%s"
+                  (git-get-committer-name) (git-get-committer-email)
+                  (if merge-heads
+                      (format "Parent: %s\n%s\n"
+                              (git-rev-parse "HEAD")
+                              (mapconcat (lambda (str) (concat "Parent: " str)) merge-heads "\n"))
+                    ""))
+          'face 'git-header-face)
+         (propertize git-log-msg-separator 'face 'git-separator-face)
+         "\n")
+        (when (and merge-heads (file-readable-p ".git/MERGE_MSG"))
+          (insert-file-contents ".git/MERGE_MSG"))))
+      (log-edit #'git-do-commit nil #'git-log-edit-files buffer)))
+
+(defun git-find-file ()
+  "Visit the current file in its own buffer."
+  (interactive)
+  (unless git-status (error "Not in git-status buffer."))
+  (let ((info (ewoc-data (ewoc-locate git-status))))
+    (find-file (git-fileinfo->name info))
+    (when (eq 'unmerged (git-fileinfo->state info))
+      (smerge-mode))))
+
+(defun git-find-file-imerge ()
+  "Visit the current file in interactive merge mode."
+  (interactive)
+  (unless git-status (error "Not in git-status buffer."))
+  (let ((info (ewoc-data (ewoc-locate git-status))))
+    (find-file (git-fileinfo->name info))
+    (smerge-ediff)))
+
+(defun git-view-file ()
+  "View the current file in its own buffer."
+  (interactive)
+  (unless git-status (error "Not in git-status buffer."))
+  (let ((info (ewoc-data (ewoc-locate git-status))))
+    (view-file (git-fileinfo->name info))))
+
+(defun git-refresh-status ()
+  "Refresh the git status buffer."
+  (interactive)
+  (let* ((status git-status)
+         (pos (ewoc-locate status))
+         (cur-name (and pos (git-fileinfo->name (ewoc-data pos)))))
+    (unless status (error "Not in git-status buffer."))
+    (git-clear-status status)
+    (git-run-command nil nil "update-index" "--info-only" "--refresh")
+    (if (git-empty-db-p)
+        ; we need some special handling for an empty db
+        (with-temp-buffer
+          (git-run-command t nil "ls-files" "-z" "-t" "-c")
+          (git-parse-ls-files status 'added))
+      (with-temp-buffer
+        (git-run-command t nil "diff-index" "-z" "-M" "HEAD")
+        (git-parse-status status)))
+      (with-temp-buffer
+        (git-run-command t nil "ls-files" "-z" "-u")
+        (git-parse-ls-unmerged status))
+      (when (file-readable-p ".git/info/exclude")
+        (with-temp-buffer
+          (git-run-command t nil "ls-files" "-z" "-t" "-o"
+                           "--exclude-from=.git/info/exclude"
+                           (concat "--exclude-per-directory=" git-per-dir-ignore-file))
+          (git-parse-ls-files status 'unknown)))
+    (git-refresh-files)
+    (git-refresh-ewoc-hf status)
+    ; move point to the current file name if any
+    (let ((node (and cur-name (git-find-status-file status cur-name))))
+      (when node (ewoc-goto-node status node)))))
+
+(defun git-status-quit ()
+  "Quit git-status mode."
+  (interactive)
+  (bury-buffer))
+
+;;;; Major Mode
+;;;; ------------------------------------------------------------
+
+(defvar git-status-mode-hook nil
+  "Run after `git-status-mode' is setup.")
+
+(defvar git-status-mode-map nil
+  "Keymap for git major mode.")
+
+(defvar git-status nil
+  "List of all files managed by the git-status mode.")
+
+(unless git-status-mode-map
+  (let ((map (make-keymap))
+        (diff-map (make-sparse-keymap)))
+    (suppress-keymap map)
+    (define-key map " "   'git-next-file)
+    (define-key map "a"   'git-add-file)
+    (define-key map "c"   'git-commit-file)
+    (define-key map "d"    diff-map)
+    (define-key map "="   'git-diff-file)
+    (define-key map "f"   'git-find-file)
+    (define-key map [RET] 'git-find-file)
+    (define-key map "g"   'git-refresh-status)
+    (define-key map "i"   'git-ignore-file)
+    (define-key map "l"   'git-log-file)
+    (define-key map "m"   'git-mark-file)
+    (define-key map "M"   'git-mark-all)
+    (define-key map "n"   'git-next-file)
+    (define-key map "p"   'git-prev-file)
+    (define-key map "q"   'git-status-quit)
+    (define-key map "r"   'git-remove-file)
+    (define-key map "R"   'git-resolve-file)
+    (define-key map "T"   'git-toggle-all-marks)
+    (define-key map "u"   'git-unmark-file)
+    (define-key map "U"   'git-revert-file)
+    (define-key map "v"   'git-view-file)
+    (define-key map "x"   'git-remove-handled)
+    (define-key map "\C-?" 'git-unmark-file-up)
+    (define-key map "\M-\C-?" 'git-unmark-all)
+    ; the diff submap
+    (define-key diff-map "b" 'git-diff-file-base)
+    (define-key diff-map "c" 'git-diff-file-combined)
+    (define-key diff-map "=" 'git-diff-file)
+    (define-key diff-map "e" 'git-diff-file-idiff)
+    (define-key diff-map "E" 'git-find-file-imerge)
+    (define-key diff-map "m" 'git-diff-file-mine)
+    (define-key diff-map "o" 'git-diff-file-other)
+    (setq git-status-mode-map map)))
+
+;; git mode should only run in the *git status* buffer
+(put 'git-status-mode 'mode-class 'special)
+
+(defun git-status-mode ()
+  "Major mode for interacting with Git.
+Commands:
+\\{git-status-mode-map}"
+  (kill-all-local-variables)
+  (buffer-disable-undo)
+  (setq mode-name "git status"
+        major-mode 'git-status-mode
+        goal-column 17
+        buffer-read-only t)
+  (use-local-map git-status-mode-map)
+  (let ((buffer-read-only nil))
+    (erase-buffer)
+  (let ((status (ewoc-create 'git-fileinfo-prettyprint "" "")))
+    (set (make-local-variable 'git-status) status))
+  (run-hooks 'git-status-mode-hook)))
+
+(defun git-status (dir)
+  "Entry point into git-status mode."
+  (interactive "DSelect directory: ")
+  (setq dir (git-get-top-dir dir))
+  (if (file-directory-p (concat (file-name-as-directory dir) ".git"))
+      (let ((buffer (create-file-buffer (expand-file-name "*git-status*" dir))))
+        (switch-to-buffer buffer)
+        (git-status-mode)
+        (cd dir)
+        (git-refresh-status)
+        (goto-char (point-min)))
+    (message "%s is not a git working tree." dir)))
+
+(provide 'git)
+;;; git.el ends here
diff --git a/contrib/git-svn/.gitignore b/contrib/git-svn/.gitignore
new file mode 100644 (file)
index 0000000..d8d87e3
--- /dev/null
@@ -0,0 +1,4 @@
+git-svn
+git-svn.xml
+git-svn.html
+git-svn.1
diff --git a/contrib/git-svn/Makefile b/contrib/git-svn/Makefile
new file mode 100644 (file)
index 0000000..a330c61
--- /dev/null
@@ -0,0 +1,32 @@
+all: git-svn
+
+prefix?=$(HOME)
+bindir=$(prefix)/bin
+mandir=$(prefix)/man
+man1=$(mandir)/man1
+INSTALL?=install
+doc_conf=../../Documentation/asciidoc.conf
+-include ../../config.mak
+
+git-svn: git-svn.perl
+       cp $< $@
+       chmod +x $@
+
+install: all
+       $(INSTALL) -d -m755 $(DESTDIR)$(bindir)
+       $(INSTALL) git-svn $(DESTDIR)$(bindir)
+
+install-doc: doc
+       $(INSTALL) git-svn.1 $(DESTDIR)$(man1)
+
+doc: git-svn.1
+git-svn.1 : git-svn.xml
+       xmlto man git-svn.xml
+git-svn.xml : git-svn.txt
+       asciidoc -b docbook -d manpage \
+               -f ../../Documentation/asciidoc.conf $<
+test:
+       cd t && $(SHELL) ./t0000-contrib-git-svn.sh
+
+clean:
+       rm -f git-svn *.xml *.html *.1
diff --git a/contrib/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl
new file mode 100755 (executable)
index 0000000..0b74165
--- /dev/null
@@ -0,0 +1,967 @@
+#!/usr/bin/env perl
+# Copyright (C) 2006, Eric Wong <normalperson@yhbt.net>
+# License: GPL v2 or later
+use warnings;
+use strict;
+use vars qw/   $AUTHOR $VERSION
+               $SVN_URL $SVN_INFO $SVN_WC
+               $GIT_SVN_INDEX $GIT_SVN
+               $GIT_DIR $REV_DIR/;
+$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
+$VERSION = '0.10.0';
+$GIT_DIR = $ENV{GIT_DIR} || "$ENV{PWD}/.git";
+$GIT_SVN = $ENV{GIT_SVN_ID} || 'git-svn';
+$GIT_SVN_INDEX = "$GIT_DIR/$GIT_SVN/index";
+$ENV{GIT_DIR} ||= $GIT_DIR;
+$SVN_URL = undef;
+$REV_DIR = "$GIT_DIR/$GIT_SVN/revs";
+$SVN_WC = "$GIT_DIR/$GIT_SVN/tree";
+
+# make sure the svn binary gives consistent output between locales and TZs:
+$ENV{TZ} = 'UTC';
+$ENV{LC_ALL} = 'C';
+
+# If SVN:: library support is added, please make the dependencies
+# optional and preserve the capability to use the command-line client.
+# use eval { require SVN::... } to make it lazy load
+use Carp qw/croak/;
+use IO::File qw//;
+use File::Basename qw/dirname basename/;
+use File::Path qw/mkpath/;
+use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
+use File::Spec qw//;
+use POSIX qw/strftime/;
+my $sha1 = qr/[a-f\d]{40}/;
+my $sha1_short = qr/[a-f\d]{6,40}/;
+my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit,
+       $_find_copies_harder, $_l, $_version);
+
+GetOptions(    'revision|r=s' => \$_revision,
+               'no-ignore-externals' => \$_no_ignore_ext,
+               'stdin|' => \$_stdin,
+               'edit|e' => \$_edit,
+               'rmdir' => \$_rmdir,
+               'help|H|h' => \$_help,
+               'find-copies-harder' => \$_find_copies_harder,
+               'l=i' => \$_l,
+               'version|V' => \$_version,
+               'no-stop-on-copy' => \$_no_stop_copy );
+my %cmd = (
+       fetch => [ \&fetch, "Download new revisions from SVN" ],
+       init => [ \&init, "Initialize and fetch (import)"],
+       commit => [ \&commit, "Commit git revisions to SVN" ],
+       'show-ignore' => [ \&show_ignore, "Show svn:ignore listings" ],
+       rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)" ],
+       help => [ \&usage, "Show help" ],
+);
+my $cmd;
+for (my $i = 0; $i < @ARGV; $i++) {
+       if (defined $cmd{$ARGV[$i]}) {
+               $cmd = $ARGV[$i];
+               splice @ARGV, $i, 1;
+               last;
+       }
+};
+
+# we may be called as git-svn-(command), or git-svn(command).
+foreach (keys %cmd) {
+       if (/git\-svn\-?($_)(?:\.\w+)?$/) {
+               $cmd = $1;
+               last;
+       }
+}
+usage(0) if $_help;
+version() if $_version;
+usage(1) unless (defined $cmd);
+svn_check_ignore_externals();
+$cmd{$cmd}->[0]->(@ARGV);
+exit 0;
+
+####################### primary functions ######################
+sub usage {
+       my $exit = shift || 0;
+       my $fd = $exit ? \*STDERR : \*STDOUT;
+       print $fd <<"";
+git-svn - bidirectional operations between a single Subversion tree and git
+Usage: $0 <command> [options] [arguments]\n
+Available commands:
+
+       foreach (sort keys %cmd) {
+               print $fd '  ',pack('A10',$_),$cmd{$_}->[1],"\n";
+       }
+       print $fd <<"";
+\nGIT_SVN_ID may be set in the environment to an arbitrary identifier if
+you're tracking multiple SVN branches/repositories in one git repository
+and want to keep them separate.
+
+       exit $exit;
+}
+
+sub version {
+       print "git-svn version $VERSION\n";
+       exit 0;
+}
+
+sub rebuild {
+       $SVN_URL = shift or undef;
+       my $repo_uuid;
+       my $newest_rev = 0;
+
+       my $pid = open(my $rev_list,'-|');
+       defined $pid or croak $!;
+       if ($pid == 0) {
+               exec("git-rev-list","$GIT_SVN-HEAD") or croak $!;
+       }
+       my $first;
+       while (<$rev_list>) {
+               chomp;
+               my $c = $_;
+               croak "Non-SHA1: $c\n" unless $c =~ /^$sha1$/o;
+               my @commit = grep(/^git-svn-id: /,`git-cat-file commit $c`);
+               next if (!@commit); # skip merges
+               my $id = $commit[$#commit];
+               my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
+                                               \s([a-f\d\-]+)$/x);
+               if (!$rev || !$uuid || !$url) {
+                       # some of the original repositories I made had
+                       # indentifiers like this:
+                       ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)
+                                                       \@([a-f\d\-]+)/x);
+                       if (!$rev || !$uuid) {
+                               croak "Unable to extract revision or UUID from ",
+                                       "$c, $id\n";
+                       }
+               }
+               print "r$rev = $c\n";
+               unless (defined $first) {
+                       if (!$SVN_URL && !$url) {
+                               croak "SVN repository location required: $url\n";
+                       }
+                       $SVN_URL ||= $url;
+                       $repo_uuid = setup_git_svn();
+                       $first = $rev;
+               }
+               if ($uuid ne $repo_uuid) {
+                       croak "Repository UUIDs do not match!\ngot: $uuid\n",
+                                               "expected: $repo_uuid\n";
+               }
+               assert_revision_eq_or_unknown($rev, $c);
+               sys('git-update-ref',"$GIT_SVN/revs/$rev",$c);
+               $newest_rev = $rev if ($rev > $newest_rev);
+       }
+       close $rev_list or croak $?;
+       if (!chdir $SVN_WC) {
+               my @svn_co = ('svn','co',"-r$first");
+               push @svn_co, '--ignore-externals' unless $_no_ignore_ext;
+               sys(@svn_co, $SVN_URL, $SVN_WC);
+               chdir $SVN_WC or croak $!;
+       }
+
+       $pid = fork;
+       defined $pid or croak $!;
+       if ($pid == 0) {
+               my @svn_up = qw(svn up);
+               push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
+               sys(@svn_up,"-r$newest_rev");
+               $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
+               git_addremove();
+               exec('git-write-tree');
+       }
+       waitpid $pid, 0;
+}
+
+sub init {
+       $SVN_URL = shift or croak "SVN repository location required\n";
+       unless (-d $GIT_DIR) {
+               sys('git-init-db');
+       }
+       setup_git_svn();
+}
+
+sub fetch {
+       my (@parents) = @_;
+       $SVN_URL ||= file_to_s("$GIT_DIR/$GIT_SVN/info/url");
+       my @log_args = -d $SVN_WC ? ($SVN_WC) : ($SVN_URL);
+       unless ($_revision) {
+               $_revision = -d $SVN_WC ? 'BASE:HEAD' : '0:HEAD';
+       }
+       push @log_args, "-r$_revision";
+       push @log_args, '--stop-on-copy' unless $_no_stop_copy;
+
+       my $svn_log = svn_log_raw(@log_args);
+       @$svn_log = sort { $a->{revision} <=> $b->{revision} } @$svn_log;
+
+       my $base = shift @$svn_log or croak "No base revision!\n";
+       my $last_commit = undef;
+       unless (-d $SVN_WC) {
+               my @svn_co = ('svn','co',"-r$base->{revision}");
+               push @svn_co,'--ignore-externals' unless $_no_ignore_ext;
+               sys(@svn_co, $SVN_URL, $SVN_WC);
+               chdir $SVN_WC or croak $!;
+               $last_commit = git_commit($base, @parents);
+               unless (-f "$GIT_DIR/refs/heads/master") {
+                       sys(qw(git-update-ref refs/heads/master),$last_commit);
+               }
+               assert_svn_wc_clean($base->{revision}, $last_commit);
+       } else {
+               chdir $SVN_WC or croak $!;
+               $last_commit = file_to_s("$REV_DIR/$base->{revision}");
+       }
+       my @svn_up = qw(svn up);
+       push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
+       my $last_rev = $base->{revision};
+       foreach my $log_msg (@$svn_log) {
+               assert_svn_wc_clean($last_rev, $last_commit);
+               $last_rev = $log_msg->{revision};
+               sys(@svn_up,"-r$last_rev");
+               $last_commit = git_commit($log_msg, $last_commit, @parents);
+       }
+       assert_svn_wc_clean($last_rev, $last_commit);
+       return pop @$svn_log;
+}
+
+sub commit {
+       my (@commits) = @_;
+       if ($_stdin || !@commits) {
+               print "Reading from stdin...\n";
+               @commits = ();
+               while (<STDIN>) {
+                       if (/\b([a-f\d]{6,40})\b/) {
+                               unshift @commits, $1;
+                       }
+               }
+       }
+       my @revs;
+       foreach my $c (@commits) {
+               chomp(my @tmp = safe_qx('git-rev-parse',$c));
+               if (scalar @tmp == 1) {
+                       push @revs, $tmp[0];
+               } elsif (scalar @tmp > 1) {
+                       push @revs, reverse (safe_qx('git-rev-list',@tmp));
+               } else {
+                       die "Failed to rev-parse $c\n";
+               }
+       }
+       chomp @revs;
+
+       fetch();
+       chdir $SVN_WC or croak $!;
+       my $svn_current_rev =  svn_info('.')->{'Last Changed Rev'};
+       foreach my $c (@revs) {
+               print "Committing $c\n";
+               my $mods = svn_checkout_tree($svn_current_rev, $c);
+               if (scalar @$mods == 0) {
+                       print "Skipping, no changes detected\n";
+                       next;
+               }
+               $svn_current_rev = svn_commit_tree($svn_current_rev, $c);
+       }
+       print "Done committing ",scalar @revs," revisions to SVN\n";
+
+}
+
+sub show_ignore {
+       require File::Find or die $!;
+       my $exclude_file = "$GIT_DIR/info/exclude";
+       open my $fh, '<', $exclude_file or croak $!;
+       chomp(my @excludes = (<$fh>));
+       close $fh or croak $!;
+
+       $SVN_URL ||= file_to_s("$GIT_DIR/$GIT_SVN/info/url");
+       chdir $SVN_WC or croak $!;
+       my %ign;
+       File::Find::find({wanted=>sub{if(lstat $_ && -d _ && -d "$_/.svn"){
+               s#^\./##;
+               @{$ign{$_}} = safe_qx(qw(svn propget svn:ignore),$_);
+               }}, no_chdir=>1},'.');
+
+       print "\n# /\n";
+       foreach (@{$ign{'.'}}) { print '/',$_ if /\S/ }
+       delete $ign{'.'};
+       foreach my $i (sort keys %ign) {
+               print "\n# ",$i,"\n";
+               foreach (@{$ign{$i}}) { print '/',$i,'/',$_ if /\S/ }
+       }
+}
+
+########################### utility functions #########################
+
+sub setup_git_svn {
+       defined $SVN_URL or croak "SVN repository location required\n";
+       unless (-d $GIT_DIR) {
+               croak "GIT_DIR=$GIT_DIR does not exist!\n";
+       }
+       mkpath(["$GIT_DIR/$GIT_SVN"]);
+       mkpath(["$GIT_DIR/$GIT_SVN/info"]);
+       mkpath([$REV_DIR]);
+       s_to_file($SVN_URL,"$GIT_DIR/$GIT_SVN/info/url");
+       my $uuid = svn_info($SVN_URL)->{'Repository UUID'} or
+                                       croak "Repository UUID unreadable\n";
+       s_to_file($uuid,"$GIT_DIR/$GIT_SVN/info/uuid");
+
+       open my $fd, '>>', "$GIT_DIR/$GIT_SVN/info/exclude" or croak $!;
+       print $fd '.svn',"\n";
+       close $fd or croak $!;
+       return $uuid;
+}
+
+sub assert_svn_wc_clean {
+       my ($svn_rev, $treeish) = @_;
+       croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/);
+       croak "$treeish is not a sha1!\n" unless ($treeish =~ /^$sha1$/o);
+       my $svn_info = svn_info('.');
+       if ($svn_rev != $svn_info->{'Last Changed Rev'}) {
+               croak "Expected r$svn_rev, got r",
+                               $svn_info->{'Last Changed Rev'},"\n";
+       }
+       my @status = grep(!/^Performing status on external/,(`svn status`));
+       @status = grep(!/^\s*$/,@status);
+       if (scalar @status) {
+               print STDERR "Tree ($SVN_WC) is not clean:\n";
+               print STDERR $_ foreach @status;
+               croak;
+       }
+       assert_tree($treeish);
+}
+
+sub assert_tree {
+       my ($treeish) = @_;
+       croak "Not a sha1: $treeish\n" unless $treeish =~ /^$sha1$/o;
+       chomp(my $type = `git-cat-file -t $treeish`);
+       my $expected;
+       while ($type eq 'tag') {
+               chomp(($treeish, $type) = `git-cat-file tag $treeish`);
+       }
+       if ($type eq 'commit') {
+               $expected = (grep /^tree /,`git-cat-file commit $treeish`)[0];
+               ($expected) = ($expected =~ /^tree ($sha1)$/);
+               die "Unable to get tree from $treeish\n" unless $expected;
+       } elsif ($type eq 'tree') {
+               $expected = $treeish;
+       } else {
+               die "$treeish is a $type, expected tree, tag or commit\n";
+       }
+
+       my $old_index = $ENV{GIT_INDEX_FILE};
+       my $tmpindex = $GIT_SVN_INDEX.'.assert-tmp';
+       if (-e $tmpindex) {
+               unlink $tmpindex or croak $!;
+       }
+       $ENV{GIT_INDEX_FILE} = $tmpindex;
+       git_addremove();
+       chomp(my $tree = `git-write-tree`);
+       if ($old_index) {
+               $ENV{GIT_INDEX_FILE} = $old_index;
+       } else {
+               delete $ENV{GIT_INDEX_FILE};
+       }
+       if ($tree ne $expected) {
+               croak "Tree mismatch, Got: $tree, Expected: $expected\n";
+       }
+}
+
+sub parse_diff_tree {
+       my $diff_fh = shift;
+       local $/ = "\0";
+       my $state = 'meta';
+       my @mods;
+       while (<$diff_fh>) {
+               chomp $_; # this gets rid of the trailing "\0"
+               if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
+                                       $sha1\s($sha1)\s([MTCRAD])\d*$/xo) {
+                       push @mods, {   mode_a => $1, mode_b => $2,
+                                       sha1_b => $3, chg => $4 };
+                       if ($4 =~ /^(?:C|R)$/) {
+                               $state = 'file_a';
+                       } else {
+                               $state = 'file_b';
+                       }
+               } elsif ($state eq 'file_a') {
+                       my $x = $mods[$#mods] or croak "Empty array\n";
+                       if ($x->{chg} !~ /^(?:C|R)$/) {
+                               croak "Error parsing $_, $x->{chg}\n";
+                       }
+                       $x->{file_a} = $_;
+                       $state = 'file_b';
+               } elsif ($state eq 'file_b') {
+                       my $x = $mods[$#mods] or croak "Empty array\n";
+                       if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
+                               croak "Error parsing $_, $x->{chg}\n";
+                       }
+                       if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
+                               croak "Error parsing $_, $x->{chg}\n";
+                       }
+                       $x->{file_b} = $_;
+                       $state = 'meta';
+               } else {
+                       croak "Error parsing $_\n";
+               }
+       }
+       close $diff_fh or croak $!;
+
+       return \@mods;
+}
+
+sub svn_check_prop_executable {
+       my $m = shift;
+       return if -l $m->{file_b};
+       if ($m->{mode_b} =~ /755$/) {
+               chmod((0755 &~ umask),$m->{file_b}) or croak $!;
+               if ($m->{mode_a} !~ /755$/) {
+                       sys(qw(svn propset svn:executable 1), $m->{file_b});
+               }
+               -x $m->{file_b} or croak "$m->{file_b} is not executable!\n";
+       } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
+               sys(qw(svn propdel svn:executable), $m->{file_b});
+               chmod((0644 &~ umask),$m->{file_b}) or croak $!;
+               -x $m->{file_b} and croak "$m->{file_b} is executable!\n";
+       }
+}
+
+sub svn_ensure_parent_path {
+       my $dir_b = dirname(shift);
+       svn_ensure_parent_path($dir_b) if ($dir_b ne File::Spec->curdir);
+       mkpath([$dir_b]) unless (-d $dir_b);
+       sys(qw(svn add -N), $dir_b) unless (-d "$dir_b/.svn");
+}
+
+sub precommit_check {
+       my $mods = shift;
+       my (%rm_file, %rmdir_check, %added_check);
+
+       my %o = ( D => 0, R => 1, C => 2, A => 3, M => 3, T => 3 );
+       foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
+               if ($m->{chg} eq 'R') {
+                       if (-d $m->{file_b}) {
+                               err_dir_to_file("$m->{file_a} => $m->{file_b}");
+                       }
+                       # dir/$file => dir/file/$file
+                       my $dirname = dirname($m->{file_b});
+                       while ($dirname ne File::Spec->curdir) {
+                               if ($dirname ne $m->{file_a}) {
+                                       $dirname = dirname($dirname);
+                                       next;
+                               }
+                               err_file_to_dir("$m->{file_a} => $m->{file_b}");
+                       }
+                       # baz/zzz => baz (baz is a file)
+                       $dirname = dirname($m->{file_a});
+                       while ($dirname ne File::Spec->curdir) {
+                               if ($dirname ne $m->{file_b}) {
+                                       $dirname = dirname($dirname);
+                                       next;
+                               }
+                               err_dir_to_file("$m->{file_a} => $m->{file_b}");
+                       }
+               }
+               if ($m->{chg} =~ /^(D|R)$/) {
+                       my $t = $1 eq 'D' ? 'file_b' : 'file_a';
+                       $rm_file{ $m->{$t} } = 1;
+                       my $dirname = dirname( $m->{$t} );
+                       my $basename = basename( $m->{$t} );
+                       $rmdir_check{$dirname}->{$basename} = 1;
+               } elsif ($m->{chg} =~ /^(?:A|C)$/) {
+                       if (-d $m->{file_b}) {
+                               err_dir_to_file($m->{file_b});
+                       }
+                       my $dirname = dirname( $m->{file_b} );
+                       my $basename = basename( $m->{file_b} );
+                       $added_check{$dirname}->{$basename} = 1;
+                       while ($dirname ne File::Spec->curdir) {
+                               if ($rm_file{$dirname}) {
+                                       err_file_to_dir($m->{file_b});
+                               }
+                               $dirname = dirname $dirname;
+                       }
+               }
+       }
+       return (\%rmdir_check, \%added_check);
+
+       sub err_dir_to_file {
+               my $file = shift;
+               print STDERR "Node change from directory to file ",
+                               "is not supported by Subversion: ",$file,"\n";
+               exit 1;
+       }
+       sub err_file_to_dir {
+               my $file = shift;
+               print STDERR "Node change from file to directory ",
+                               "is not supported by Subversion: ",$file,"\n";
+               exit 1;
+       }
+}
+
+sub svn_checkout_tree {
+       my ($svn_rev, $treeish) = @_;
+       my $from = file_to_s("$REV_DIR/$svn_rev");
+       assert_svn_wc_clean($svn_rev,$from);
+       print "diff-tree '$from' '$treeish'\n";
+       my $pid = open my $diff_fh, '-|';
+       defined $pid or croak $!;
+       if ($pid == 0) {
+               my @diff_tree = qw(git-diff-tree -z -r -C);
+               push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
+               push @diff_tree, "-l$_l" if defined $_l;
+               exec(@diff_tree, $from, $treeish) or croak $!;
+       }
+       my $mods = parse_diff_tree($diff_fh);
+       unless (@$mods) {
+               # git can do empty commits, SVN doesn't allow it...
+               return $mods;
+       }
+       my ($rm, $add) = precommit_check($mods);
+
+       my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
+       foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
+               if ($m->{chg} eq 'C') {
+                       svn_ensure_parent_path( $m->{file_b} );
+                       sys(qw(svn cp),         $m->{file_a}, $m->{file_b});
+                       apply_mod_line_blob($m);
+                       svn_check_prop_executable($m);
+               } elsif ($m->{chg} eq 'D') {
+                       sys(qw(svn rm --force), $m->{file_b});
+               } elsif ($m->{chg} eq 'R') {
+                       svn_ensure_parent_path( $m->{file_b} );
+                       sys(qw(svn mv --force), $m->{file_a}, $m->{file_b});
+                       apply_mod_line_blob($m);
+                       svn_check_prop_executable($m);
+               } elsif ($m->{chg} eq 'M') {
+                       apply_mod_line_blob($m);
+                       svn_check_prop_executable($m);
+               } elsif ($m->{chg} eq 'T') {
+                       sys(qw(svn rm --force),$m->{file_b});
+                       apply_mod_line_blob($m);
+                       sys(qw(svn add --force), $m->{file_b});
+                       svn_check_prop_executable($m);
+               } elsif ($m->{chg} eq 'A') {
+                       svn_ensure_parent_path( $m->{file_b} );
+                       apply_mod_line_blob($m);
+                       sys(qw(svn add --force), $m->{file_b});
+                       svn_check_prop_executable($m);
+               } else {
+                       croak "Invalid chg: $m->{chg}\n";
+               }
+       }
+
+       assert_tree($treeish);
+       if ($_rmdir) { # remove empty directories
+               handle_rmdir($rm, $add);
+       }
+       assert_tree($treeish);
+       return $mods;
+}
+
+# svn ls doesn't work with respect to the current working tree, but what's
+# in the repository.  There's not even an option for it... *sigh*
+# (added files don't show up and removed files remain in the ls listing)
+sub svn_ls_current {
+       my ($dir, $rm, $add) = @_;
+       chomp(my @ls = safe_qx('svn','ls',$dir));
+       my @ret = ();
+       foreach (@ls) {
+               s#/$##; # trailing slashes are evil
+               push @ret, $_ unless $rm->{$dir}->{$_};
+       }
+       if (exists $add->{$dir}) {
+               push @ret, keys %{$add->{$dir}};
+       }
+       return \@ret;
+}
+
+sub handle_rmdir {
+       my ($rm, $add) = @_;
+
+       foreach my $dir (sort {length $b <=> length $a} keys %$rm) {
+               my $ls = svn_ls_current($dir, $rm, $add);
+               next if (scalar @$ls);
+               sys(qw(svn rm --force),$dir);
+
+               my $dn = dirname $dir;
+               $rm->{ $dn }->{ basename $dir } = 1;
+               $ls = svn_ls_current($dn, $rm, $add);
+               while (scalar @$ls == 0 && $dn ne File::Spec->curdir) {
+                       sys(qw(svn rm --force),$dn);
+                       $dir = basename $dn;
+                       $dn = dirname $dn;
+                       $rm->{ $dn }->{ $dir } = 1;
+                       $ls = svn_ls_current($dn, $rm, $add);
+               }
+       }
+}
+
+sub svn_commit_tree {
+       my ($svn_rev, $commit) = @_;
+       my $commit_msg = "$GIT_DIR/$GIT_SVN/.svn-commit.tmp.$$";
+       my %log_msg = ( msg => '' );
+       open my $msg, '>', $commit_msg  or croak $!;
+
+       chomp(my $type = `git-cat-file -t $commit`);
+       if ($type eq 'commit') {
+               my $pid = open my $msg_fh, '-|';
+               defined $pid or croak $!;
+
+               if ($pid == 0) {
+                       exec(qw(git-cat-file commit), $commit) or croak $!;
+               }
+               my $in_msg = 0;
+               while (<$msg_fh>) {
+                       if (!$in_msg) {
+                               $in_msg = 1 if (/^\s*$/);
+                       } else {
+                               $log_msg{msg} .= $_;
+                               print $msg $_ or croak $!;
+                       }
+               }
+               close $msg_fh or croak $!;
+       }
+       close $msg or croak $!;
+
+       if ($_edit || ($type eq 'tree')) {
+               my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi';
+               system($editor, $commit_msg);
+       }
+       my @ci_output = safe_qx(qw(svn commit -F),$commit_msg);
+       my ($committed) = grep(/^Committed revision \d+\./,@ci_output);
+       unlink $commit_msg;
+       defined $committed or croak
+                       "Commit output failed to parse committed revision!\n",
+                       join("\n",@ci_output),"\n";
+       my ($rev_committed) = ($committed =~ /^Committed revision (\d+)\./);
+
+       my @svn_up = qw(svn up);
+       push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
+       if ($rev_committed == ($svn_rev + 1)) {
+               push @svn_up, "-r$rev_committed";
+               sys(@svn_up);
+               my $info = svn_info('.');
+               my $date = $info->{'Last Changed Date'} or die "Missing date\n";
+               if ($info->{'Last Changed Rev'} != $rev_committed) {
+                       croak "$info->{'Last Changed Rev'} != $rev_committed\n"
+               }
+               my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
+                                       /(\d{4})\-(\d\d)\-(\d\d)\s
+                                        (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
+                                        or croak "Failed to parse date: $date\n";
+               $log_msg{date} = "$tz $Y-$m-$d $H:$M:$S";
+               $log_msg{author} = $info->{'Last Changed Author'};
+               $log_msg{revision} = $rev_committed;
+               $log_msg{msg} .= "\n";
+               my $parent = file_to_s("$REV_DIR/$svn_rev");
+               git_commit(\%log_msg, $parent, $commit);
+               return $rev_committed;
+       }
+       # resync immediately
+       push @svn_up, "-r$svn_rev";
+       sys(@svn_up);
+       return fetch("$rev_committed=$commit")->{revision};
+}
+
+sub svn_log_raw {
+       my (@log_args) = @_;
+       my $pid = open my $log_fh,'-|';
+       defined $pid or croak $!;
+
+       if ($pid == 0) {
+               exec (qw(svn log), @log_args) or croak $!
+       }
+
+       my @svn_log;
+       my $state = 'sep';
+       while (<$log_fh>) {
+               chomp;
+               if (/^\-{72}$/) {
+                       if ($state eq 'msg') {
+                               if ($svn_log[$#svn_log]->{lines}) {
+                                       $svn_log[$#svn_log]->{msg} .= $_."\n";
+                                       unless(--$svn_log[$#svn_log]->{lines}) {
+                                               $state = 'sep';
+                                       }
+                               } else {
+                                       croak "Log parse error at: $_\n",
+                                               $svn_log[$#svn_log]->{revision},
+                                               "\n";
+                               }
+                               next;
+                       }
+                       if ($state ne 'sep') {
+                               croak "Log parse error at: $_\n",
+                                       "state: $state\n",
+                                       $svn_log[$#svn_log]->{revision},
+                                       "\n";
+                       }
+                       $state = 'rev';
+
+                       # if we have an empty log message, put something there:
+                       if (@svn_log) {
+                               $svn_log[$#svn_log]->{msg} ||= "\n";
+                               delete $svn_log[$#svn_log]->{lines};
+                       }
+                       next;
+               }
+               if ($state eq 'rev' && s/^r(\d+)\s*\|\s*//) {
+                       my $rev = $1;
+                       my ($author, $date, $lines) = split(/\s*\|\s*/, $_, 3);
+                       ($lines) = ($lines =~ /(\d+)/);
+                       my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
+                                       /(\d{4})\-(\d\d)\-(\d\d)\s
+                                        (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
+                                        or croak "Failed to parse date: $date\n";
+                       my %log_msg = ( revision => $rev,
+                                       date => "$tz $Y-$m-$d $H:$M:$S",
+                                       author => $author,
+                                       lines => $lines,
+                                       msg => '' );
+                       push @svn_log, \%log_msg;
+                       $state = 'msg_start';
+                       next;
+               }
+               # skip the first blank line of the message:
+               if ($state eq 'msg_start' && /^$/) {
+                       $state = 'msg';
+               } elsif ($state eq 'msg') {
+                       if ($svn_log[$#svn_log]->{lines}) {
+                               $svn_log[$#svn_log]->{msg} .= $_."\n";
+                               unless (--$svn_log[$#svn_log]->{lines}) {
+                                       $state = 'sep';
+                               }
+                       } else {
+                               croak "Log parse error at: $_\n",
+                                       $svn_log[$#svn_log]->{revision},"\n";
+                       }
+               }
+       }
+       close $log_fh or croak $?;
+       return \@svn_log;
+}
+
+sub svn_info {
+       my $url = shift || $SVN_URL;
+
+       my $pid = open my $info_fh, '-|';
+       defined $pid or croak $!;
+
+       if ($pid == 0) {
+               exec(qw(svn info),$url) or croak $!;
+       }
+
+       my $ret = {};
+       # only single-lines seem to exist in svn info output
+       while (<$info_fh>) {
+               chomp $_;
+               if (m#^([^:]+)\s*:\s*(\S.*)$#) {
+                       $ret->{$1} = $2;
+                       push @{$ret->{-order}}, $1;
+               }
+       }
+       close $info_fh or croak $!;
+       return $ret;
+}
+
+sub sys { system(@_) == 0 or croak $? }
+
+sub git_addremove {
+       system( "git-diff-files --name-only -z ".
+                               " | git-update-index --remove -z --stdin && ".
+               "git-ls-files -z --others ".
+                       "'--exclude-from=$GIT_DIR/$GIT_SVN/info/exclude'".
+                               " | git-update-index --add -z --stdin"
+               ) == 0 or croak $?
+}
+
+sub s_to_file {
+       my ($str, $file, $mode) = @_;
+       open my $fd,'>',$file or croak $!;
+       print $fd $str,"\n" or croak $!;
+       close $fd or croak $!;
+       chmod ($mode &~ umask, $file) if (defined $mode);
+}
+
+sub file_to_s {
+       my $file = shift;
+       open my $fd,'<',$file or croak "$!: file: $file\n";
+       local $/;
+       my $ret = <$fd>;
+       close $fd or croak $!;
+       $ret =~ s/\s*$//s;
+       return $ret;
+}
+
+sub assert_revision_unknown {
+       my $revno = shift;
+       if (-f "$REV_DIR/$revno") {
+               croak "$REV_DIR/$revno already exists! ",
+                               "Why are we refetching it?";
+       }
+}
+
+sub assert_revision_eq_or_unknown {
+       my ($revno, $commit) = @_;
+       if (-f "$REV_DIR/$revno") {
+               my $current = file_to_s("$REV_DIR/$revno");
+               if ($commit ne $current) {
+                       croak "$REV_DIR/$revno already exists!\n",
+                               "current: $current\nexpected: $commit\n";
+               }
+               return;
+       }
+}
+
+sub git_commit {
+       my ($log_msg, @parents) = @_;
+       assert_revision_unknown($log_msg->{revision});
+       my $out_fh = IO::File->new_tmpfile or croak $!;
+       my $info = svn_info('.');
+       my $uuid = $info->{'Repository UUID'};
+       defined $uuid or croak "Unable to get Repository UUID\n";
+
+       # commit parents can be conditionally bound to a particular
+       # svn revision via: "svn_revno=commit_sha1", filter them out here:
+       my @exec_parents;
+       foreach my $p (@parents) {
+               next unless defined $p;
+               if ($p =~ /^(\d+)=($sha1_short)$/o) {
+                       if ($1 == $log_msg->{revision}) {
+                               push @exec_parents, $2;
+                       }
+               } else {
+                       push @exec_parents, $p if $p =~ /$sha1_short/o;
+               }
+       }
+
+       my $pid = fork;
+       defined $pid or croak $!;
+       if ($pid == 0) {
+               $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
+               git_addremove();
+               chomp(my $tree = `git-write-tree`);
+               croak if $?;
+               my $msg_fh = IO::File->new_tmpfile or croak $!;
+               print $msg_fh $log_msg->{msg}, "\ngit-svn-id: ",
+                                       "$SVN_URL\@$log_msg->{revision}",
+                                       " $uuid\n" or croak $!;
+               $msg_fh->flush == 0 or croak $!;
+               seek $msg_fh, 0, 0 or croak $!;
+
+               $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} =
+                                               $log_msg->{author};
+               $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} =
+                                               $log_msg->{author}."\@$uuid";
+               $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} =
+                                               $log_msg->{date};
+               my @exec = ('git-commit-tree',$tree);
+               push @exec, '-p', $_  foreach @exec_parents;
+               open STDIN, '<&', $msg_fh or croak $!;
+               open STDOUT, '>&', $out_fh or croak $!;
+               exec @exec or croak $!;
+       }
+       waitpid($pid,0);
+       croak if $?;
+
+       $out_fh->flush == 0 or croak $!;
+       seek $out_fh, 0, 0 or croak $!;
+       chomp(my $commit = do { local $/; <$out_fh> });
+       if ($commit !~ /^$sha1$/o) {
+               croak "Failed to commit, invalid sha1: $commit\n";
+       }
+       my @update_ref = ('git-update-ref',"refs/heads/$GIT_SVN-HEAD",$commit);
+       if (my $primary_parent = shift @exec_parents) {
+               push @update_ref, $primary_parent;
+       }
+       sys(@update_ref);
+       sys('git-update-ref',"$GIT_SVN/revs/$log_msg->{revision}",$commit);
+       print "r$log_msg->{revision} = $commit\n";
+       return $commit;
+}
+
+sub apply_mod_line_blob {
+       my $m = shift;
+       if ($m->{mode_b} =~ /^120/) {
+               blob_to_symlink($m->{sha1_b}, $m->{file_b});
+       } else {
+               blob_to_file($m->{sha1_b}, $m->{file_b});
+       }
+}
+
+sub blob_to_symlink {
+       my ($blob, $link) = @_;
+       defined $link or croak "\$link not defined!\n";
+       croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
+       if (-l $link || -f _) {
+               unlink $link or croak $!;
+       }
+
+       my $dest = `git-cat-file blob $blob`; # no newline, so no chomp
+       symlink $dest, $link or croak $!;
+}
+
+sub blob_to_file {
+       my ($blob, $file) = @_;
+       defined $file or croak "\$file not defined!\n";
+       croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
+       if (-l $file || -f _) {
+               unlink $file or croak $!;
+       }
+
+       open my $blob_fh, '>', $file or croak "$!: $file\n";
+       my $pid = fork;
+       defined $pid or croak $!;
+
+       if ($pid == 0) {
+               open STDOUT, '>&', $blob_fh or croak $!;
+               exec('git-cat-file','blob',$blob);
+       }
+       waitpid $pid, 0;
+       croak $? if $?;
+
+       close $blob_fh or croak $!;
+}
+
+sub safe_qx {
+       my $pid = open my $child, '-|';
+       defined $pid or croak $!;
+       if ($pid == 0) {
+               exec(@_) or croak $?;
+       }
+       my @ret = (<$child>);
+       close $child or croak $?;
+       die $? if $?; # just in case close didn't error out
+       return wantarray ? @ret : join('',@ret);
+}
+
+sub svn_check_ignore_externals {
+       return if $_no_ignore_ext;
+       unless (grep /ignore-externals/,(safe_qx(qw(svn co -h)))) {
+               print STDERR "W: Installed svn version does not support ",
+                               "--ignore-externals\n";
+               $_no_ignore_ext = 1;
+       }
+}
+__END__
+
+Data structures:
+
+@svn_log = array of log_msg hashes
+
+$log_msg hash
+{
+       msg => 'whitespace-formatted log entry
+',                                             # trailing newline is preserved
+       revision => '8',                        # integer
+       date => '2004-02-24T17:01:44.108345Z',  # commit date
+       author => 'committer name'
+};
+
+
+@mods = array of diff-index line hashes, each element represents one line
+       of diff-index output
+
+diff-index line ($m hash)
+{
+       mode_a => first column of diff-index output, no leading ':',
+       mode_b => second column of diff-index output,
+       sha1_b => sha1sum of the final blob,
+       chg => change type [MCRAD],
+       file_a => original file name of a file (iff chg is 'C' or 'R')
+       file_b => new/current file name of a file (any chg)
+}
+;
diff --git a/contrib/git-svn/git-svn.txt b/contrib/git-svn/git-svn.txt
new file mode 100644 (file)
index 0000000..b290739
--- /dev/null
@@ -0,0 +1,236 @@
+git-svn(1)
+==========
+
+NAME
+----
+git-svn - bidirectional operation between a single Subversion branch and git
+
+SYNOPSIS
+--------
+'git-svn' <command> [options] [arguments]
+
+DESCRIPTION
+-----------
+git-svn is a simple conduit for changesets between a single Subversion
+branch and git.
+
+git-svn is not to be confused with git-svnimport.  The were designed
+with very different goals in mind.
+
+git-svn is designed for an individual developer who wants a
+bidirectional flow of changesets between a single branch in Subversion
+and an arbitrary number of branches in git.  git-svnimport is designed
+for read-only operation on repositories that match a particular layout
+(albeit the recommended one by SVN developers).
+
+For importing svn, git-svnimport is potentially more powerful when
+operating on repositories organized under the recommended
+trunk/branch/tags structure, and should be faster, too.
+
+git-svn completely ignores the very limited view of branching that
+Subversion has.  This allows git-svn to be much easier to use,
+especially on repositories that are not organized in a manner that
+git-svnimport is designed for.
+
+COMMANDS
+--------
+init::
+       Creates an empty git repository with additional metadata
+       directories for git-svn.  The SVN_URL must be specified
+       at this point.
+
+fetch::
+       Fetch unfetched revisions from the SVN_URL we are tracking.
+       refs/heads/git-svn-HEAD will be updated to the latest revision.
+
+       Note: You should never attempt to modify the git-svn-HEAD branch
+       outside of git-svn.  Instead, create a branch from git-svn-HEAD
+       and work on that branch.  Use the 'commit' command (see below)
+       to write git commits back to git-svn-HEAD.
+
+commit::
+       Commit specified commit or tree objects to SVN.  This relies on
+       your imported fetch data being up-to-date.  This makes
+       absolutely no attempts to do patching when committing to SVN, it
+       simply overwrites files with those specified in the tree or
+       commit.  All merging is assumed to have taken place
+       independently of git-svn functions.
+
+rebuild::
+       Not a part of daily usage, but this is a useful command if
+       you've just cloned a repository (using git-clone) that was
+       tracked with git-svn.  Unfortunately, git-clone does not clone
+       git-svn metadata and the svn working tree that git-svn uses for
+       its operations.  This rebuilds the metadata so git-svn can
+       resume fetch operations.  SVN_URL may be optionally specified if
+       the directory/repository you're tracking has moved or changed
+       protocols.
+
+show-ignore::
+       Recursively finds and lists the svn:ignore property on
+       directories.  The output is suitable for appending to
+       the $GIT_DIR/info/exclude file.
+
+OPTIONS
+-------
+-r <ARG>::
+--revision <ARG>::
+       Only used with the 'fetch' command.
+
+       Takes any valid -r<argument> svn would accept and passes it
+       directly to svn. -r<ARG1>:<ARG2> ranges and "{" DATE "}" syntax
+       is also supported.  This is passed directly to svn, see svn
+       documentation for more details.
+
+       This can allow you to make partial mirrors when running fetch.
+
+-::
+--stdin::
+       Only used with the 'commit' command.
+
+       Read a list of commits from stdin and commit them in reverse
+       order.  Only the leading sha1 is read from each line, so
+       git-rev-list --pretty=oneline output can be used.
+
+--rmdir::
+       Only used with the 'commit' command.
+
+       Remove directories from the SVN tree if there are no files left
+       behind.  SVN can version empty directories, and they are not
+       removed by default if there are no files left in them.  git
+       cannot version empty directories.  Enabling this flag will make
+       the commit to SVN act like git.
+
+-e::
+--edit::
+       Only used with the 'commit' command.
+
+       Edit the commit message before committing to SVN.  This is off by
+       default for objects that are commits, and forced on when committing
+       tree objects.
+
+-l<num>::
+--find-copies-harder::
+       Both of these are only used with the 'commit' command.
+
+       They are both passed directly to git-diff-tree see
+       git-diff-tree(1) for more information.
+
+COMPATIBILITY OPTIONS
+---------------------
+--no-ignore-externals::
+       Only used with the 'fetch' and 'rebuild' command.
+
+       By default, git-svn passes --ignore-externals to svn to avoid
+       fetching svn:external trees into git.  Pass this flag to enable
+       externals tracking directly via git.
+
+       Versions of svn that do not support --ignore-externals are
+       automatically detected and this flag will be automatically
+       enabled for them.
+
+       Otherwise, do not enable this flag unless you know what you're
+       doing.
+
+--no-stop-on-copy::
+       Only used with the 'fetch' command.
+
+       By default, git-svn passes --stop-on-copy to avoid dealing with
+       the copied/renamed branch directory problem entirely.  A
+       copied/renamed branch is the result of a <SVN_URL> being created
+       in the past from a different source.  These are problematic to
+       deal with even when working purely with svn if you work inside
+       subdirectories.
+
+       Do not use this flag unless you know exactly what you're getting
+       yourself into.  You have been warned.
+
+Examples
+~~~~~~~~
+
+Tracking and contributing to an Subversion managed-project:
+
+# Initialize a tree (like git init-db)::
+       git-svn init http://svn.foo.org/project/trunk
+# Fetch remote revisions::
+       git-svn fetch
+# Create your own branch to hack on::
+       git checkout -b my-branch git-svn-HEAD
+# Commit only the git commits you want to SVN::
+       git-svn commit <tree-ish> [<tree-ish_2> ...]
+# Commit all the git commits from my-branch that don't exist in SVN::
+       git-svn commit git-svn-HEAD..my-branch
+# Something is committed to SVN, pull the latest into your branch::
+       git-svn fetch && git pull . git-svn-HEAD
+# Append svn:ignore settings to the default git exclude file:
+       git-svn show-ignore >> .git/info/exclude
+
+DESIGN PHILOSOPHY
+-----------------
+Merge tracking in Subversion is lacking and doing branched development
+with Subversion is cumbersome as a result.  git-svn completely forgoes
+any automated merge/branch tracking on the Subversion side and leaves it
+entirely up to the user on the git side.  It's simply not worth it to do
+a useful translation when the the original signal is weak.
+
+TRACKING MULTIPLE REPOSITORIES OR BRANCHES
+------------------------------------------
+This is for advanced users, most users should ignore this section.
+
+Because git-svn does not care about relationships between different
+branches or directories in a Subversion repository, git-svn has a simple
+hack to allow it to track an arbitrary number of related _or_ unrelated
+SVN repositories via one git repository.  Simply set the GIT_SVN_ID
+environment variable to a name other other than "git-svn" (the default)
+and git-svn will ignore the contents of the $GIT_DIR/git-svn directory
+and instead do all of its work in $GIT_DIR/$GIT_SVN_ID for that
+invocation.  The interface branch will be $GIT_SVN_ID-HEAD, instead of
+git-svn-HEAD.  Any $GIT_SVN_ID-HEAD branch should never be modified
+by the user outside of git-svn commands.
+
+ADDITIONAL FETCH ARGUMENTS
+--------------------------
+This is for advanced users, most users should ignore this section.
+
+Unfetched SVN revisions may be imported as children of existing commits
+by specifying additional arguments to 'fetch'.  Additional parents may
+optionally be specified in the form of sha1 hex sums at the
+command-line.  Unfetched SVN revisions may also be tied to particular
+git commits with the following syntax:
+
+       svn_revision_number=git_commit_sha1
+
+This allows you to tie unfetched SVN revision 375 to your current HEAD::
+
+       git-svn fetch 375=$(git-rev-parse HEAD)
+
+BUGS
+----
+If somebody commits a conflicting changeset to SVN at a bad moment
+(right before you commit) causing a conflict and your commit to fail,
+your svn working tree ($GIT_DIR/git-svn/tree) may be dirtied.  The
+easiest thing to do is probably just to rm -rf $GIT_DIR/git-svn/tree and
+run 'rebuild'.
+
+We ignore all SVN properties except svn:executable.  Too difficult to
+map them since we rely heavily on git write-tree being _exactly_ the
+same on both the SVN and git working trees and I prefer not to clutter
+working trees with metadata files.
+
+svn:keywords can't be ignored in Subversion (at least I don't know of
+a way to ignore them).
+
+Renamed and copied directories are not detected by git and hence not
+tracked when committing to SVN.  I do not plan on adding support for
+this as it's quite difficult and time-consuming to get working for all
+the possible corner cases (git doesn't do it, either).  Renamed and
+copied files are fully supported if they're similar enough for git to
+detect them.
+
+Author
+------
+Written by Eric Wong <normalperson@yhbt.net>.
+
+Documentation
+-------------
+Written by Eric Wong <normalperson@yhbt.net>.
diff --git a/contrib/git-svn/t/t0000-contrib-git-svn.sh b/contrib/git-svn/t/t0000-contrib-git-svn.sh
new file mode 100644 (file)
index 0000000..181dfe0
--- /dev/null
@@ -0,0 +1,216 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Eric Wong
+#
+
+
+PATH=$PWD/../:$PATH
+test_description='git-svn tests'
+if test -d ../../../t
+then
+    cd ../../../t
+else
+    echo "Must be run in contrib/git-svn/t" >&2
+    exit 1
+fi
+
+. ./test-lib.sh
+
+GIT_DIR=$PWD/.git
+GIT_SVN_DIR=$GIT_DIR/git-svn
+SVN_TREE=$GIT_SVN_DIR/tree
+
+svnadmin >/dev/null 2>&1
+if test $? != 1
+then
+    test_expect_success 'skipping contrib/git-svn test' :
+    test_done
+    exit
+fi
+
+svn >/dev/null 2>&1
+if test $? != 1
+then
+    test_expect_success 'skipping contrib/git-svn test' :
+    test_done
+    exit
+fi
+
+svnrepo=$PWD/svnrepo
+
+set -e
+
+svnadmin create $svnrepo
+svnrepo="file://$svnrepo/test-git-svn"
+
+mkdir import
+
+cd import
+
+echo foo > foo
+ln -s foo foo.link
+mkdir -p dir/a/b/c/d/e
+echo 'deep dir' > dir/a/b/c/d/e/file
+mkdir -p bar
+echo 'zzz' > bar/zzz
+echo '#!/bin/sh' > exec.sh
+chmod +x exec.sh
+svn import -m 'import for git-svn' . $svnrepo >/dev/null
+
+cd ..
+
+rm -rf import
+
+test_expect_success \
+    'initialize git-svn' \
+    "git-svn init $svnrepo"
+
+test_expect_success \
+    'import an SVN revision into git' \
+    'git-svn fetch'
+
+
+name='try a deep --rmdir with a commit'
+git checkout -b mybranch git-svn-HEAD
+mv dir/a/b/c/d/e/file dir/file
+cp dir/file file
+git update-index --add --remove dir/a/b/c/d/e/file dir/file file
+git commit -m "$name"
+
+test_expect_success "$name" \
+    "git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch &&
+     test -d $SVN_TREE/dir && test ! -d $SVN_TREE/dir/a"
+
+
+name='detect node change from file to directory #1'
+mkdir dir/new_file
+mv dir/file dir/new_file/file
+mv dir/new_file dir/file
+git update-index --remove dir/file
+git update-index --add dir/file/file
+git commit -m "$name"
+
+test_expect_code 1 "$name" \
+    'git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch' \
+    || true
+
+
+name='detect node change from directory to file #1'
+rm -rf dir $GIT_DIR/index
+git checkout -b mybranch2 git-svn-HEAD
+mv bar/zzz zzz
+rm -rf bar
+mv zzz bar
+git update-index --remove -- bar/zzz
+git update-index --add -- bar
+git commit -m "$name"
+
+test_expect_code 1 "$name" \
+    'git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch2' \
+    || true
+
+
+name='detect node change from file to directory #2'
+rm -f $GIT_DIR/index
+git checkout -b mybranch3 git-svn-HEAD
+rm bar/zzz
+git-update-index --remove bar/zzz
+mkdir bar/zzz
+echo yyy > bar/zzz/yyy
+git-update-index --add bar/zzz/yyy
+git commit -m "$name"
+
+test_expect_code 1 "$name" \
+    'git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch3' \
+    || true
+
+
+name='detect node change from directory to file #2'
+rm -f $GIT_DIR/index
+git checkout -b mybranch4 git-svn-HEAD
+rm -rf dir
+git update-index --remove -- dir/file
+touch dir
+echo asdf > dir
+git update-index --add -- dir
+git commit -m "$name"
+
+test_expect_code 1 "$name" \
+    'git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch4' \
+    || true
+
+
+name='remove executable bit from a file'
+rm -f $GIT_DIR/index
+git checkout -b mybranch5 git-svn-HEAD
+chmod -x exec.sh
+git update-index exec.sh
+git commit -m "$name"
+
+test_expect_success "$name" \
+    "git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch5 &&
+     test ! -x $SVN_TREE/exec.sh"
+
+
+name='add executable bit back file'
+chmod +x exec.sh
+git update-index exec.sh
+git commit -m "$name"
+
+test_expect_success "$name" \
+    "git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch5 &&
+     test -x $SVN_TREE/exec.sh"
+
+
+
+name='executable file becomes a symlink to bar/zzz (file)'
+rm exec.sh
+ln -s bar/zzz exec.sh
+git update-index exec.sh
+git commit -m "$name"
+
+test_expect_success "$name" \
+    "git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch5 &&
+     test -L $SVN_TREE/exec.sh"
+
+
+
+name='new symlink is added to a file that was also just made executable'
+chmod +x bar/zzz
+ln -s bar/zzz exec-2.sh
+git update-index --add bar/zzz exec-2.sh
+git commit -m "$name"
+
+test_expect_success "$name" \
+    "git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch5 &&
+     test -x $SVN_TREE/bar/zzz &&
+     test -L $SVN_TREE/exec-2.sh"
+
+
+
+name='modify a symlink to become a file'
+git help > help || true
+rm exec-2.sh
+cp help exec-2.sh
+git update-index exec-2.sh
+git commit -m "$name"
+
+test_expect_success "$name" \
+    "git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch5 &&
+     test -f $SVN_TREE/exec-2.sh &&
+     test ! -L $SVN_TREE/exec-2.sh &&
+     diff -u help $SVN_TREE/exec-2.sh"
+
+
+
+name='test fetch functionality (svn => git) with alternate GIT_SVN_ID'
+GIT_SVN_ID=alt
+export GIT_SVN_ID
+test_expect_success "$name" \
+    "git-svn init $svnrepo && git-svn fetch -v &&
+     git-rev-list --pretty=raw git-svn-HEAD | grep ^tree | uniq > a &&
+     git-rev-list --pretty=raw alt-HEAD | grep ^tree | uniq > b &&
+     diff -u a b"
+
+test_done
+
diff --git a/contrib/gitview/gitview b/contrib/gitview/gitview
new file mode 100755 (executable)
index 0000000..de9f3f3
--- /dev/null
@@ -0,0 +1,1000 @@
+#! /usr/bin/env python
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+""" gitview
+GUI browser for git repository
+This program is based on bzrk by Scott James Remnant <scott@ubuntu.com>
+"""
+__copyright__ = "Copyright (C) 2006 Hewlett-Packard Development Company, L.P."
+__author__    = "Aneesh Kumar K.V <aneesh.kumar@hp.com>"
+
+
+import sys
+import os
+import gtk
+import pygtk
+import pango
+import re
+import time
+import gobject
+import cairo
+import math
+import string
+
+try:
+    import gtksourceview
+    have_gtksourceview = True
+except ImportError:
+    have_gtksourceview = False
+    print "Running without gtksourceview module"
+
+re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
+
+def list_to_string(args, skip):
+       count = len(args)
+       i = skip
+       str_arg=" "
+       while (i < count ):
+               str_arg = str_arg + args[i]
+               str_arg = str_arg + " "
+               i = i+1
+
+       return str_arg
+
+def show_date(epoch, tz):
+       secs = float(epoch)
+       tzsecs = float(tz[1:3]) * 3600
+       tzsecs += float(tz[3:5]) * 60
+       if (tz[0] == "+"):
+               secs += tzsecs
+       else:
+               secs -= tzsecs
+
+       return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
+
+
+class CellRendererGraph(gtk.GenericCellRenderer):
+       """Cell renderer for directed graph.
+
+       This module contains the implementation of a custom GtkCellRenderer that
+       draws part of the directed graph based on the lines suggested by the code
+       in graph.py.
+
+       Because we're shiny, we use Cairo to do this, and because we're naughty
+       we cheat and draw over the bits of the TreeViewColumn that are supposed to
+       just be for the background.
+
+       Properties:
+       node              (column, colour, [ names ]) tuple to draw revision node,
+       in_lines          (start, end, colour) tuple list to draw inward lines,
+       out_lines         (start, end, colour) tuple list to draw outward lines.
+       """
+
+       __gproperties__ = {
+       "node":         ( gobject.TYPE_PYOBJECT, "node",
+                         "revision node instruction",
+                         gobject.PARAM_WRITABLE
+                       ),
+       "in-lines":     ( gobject.TYPE_PYOBJECT, "in-lines",
+                         "instructions to draw lines into the cell",
+                         gobject.PARAM_WRITABLE
+                       ),
+       "out-lines":    ( gobject.TYPE_PYOBJECT, "out-lines",
+                         "instructions to draw lines out of the cell",
+                         gobject.PARAM_WRITABLE
+                       ),
+       }
+
+       def do_set_property(self, property, value):
+               """Set properties from GObject properties."""
+               if property.name == "node":
+                       self.node = value
+               elif property.name == "in-lines":
+                       self.in_lines = value
+               elif property.name == "out-lines":
+                       self.out_lines = value
+               else:
+                       raise AttributeError, "no such property: '%s'" % property.name
+
+       def box_size(self, widget):
+               """Calculate box size based on widget's font.
+
+               Cache this as it's probably expensive to get.  It ensures that we
+               draw the graph at least as large as the text.
+               """
+               try:
+                       return self._box_size
+               except AttributeError:
+                       pango_ctx = widget.get_pango_context()
+                       font_desc = widget.get_style().font_desc
+                       metrics = pango_ctx.get_metrics(font_desc)
+
+                       ascent = pango.PIXELS(metrics.get_ascent())
+                       descent = pango.PIXELS(metrics.get_descent())
+
+                       self._box_size = ascent + descent + 6
+                       return self._box_size
+
+       def set_colour(self, ctx, colour, bg, fg):
+               """Set the context source colour.
+
+               Picks a distinct colour based on an internal wheel; the bg
+               parameter provides the value that should be assigned to the 'zero'
+               colours and the fg parameter provides the multiplier that should be
+               applied to the foreground colours.
+               """
+               colours = [
+                   ( 1.0, 0.0, 0.0 ),
+                   ( 1.0, 1.0, 0.0 ),
+                   ( 0.0, 1.0, 0.0 ),
+                   ( 0.0, 1.0, 1.0 ),
+                   ( 0.0, 0.0, 1.0 ),
+                   ( 1.0, 0.0, 1.0 ),
+                   ]
+
+               colour %= len(colours)
+               red   = (colours[colour][0] * fg) or bg
+               green = (colours[colour][1] * fg) or bg
+               blue  = (colours[colour][2] * fg) or bg
+
+               ctx.set_source_rgb(red, green, blue)
+
+       def on_get_size(self, widget, cell_area):
+               """Return the size we need for this cell.
+
+               Each cell is drawn individually and is only as wide as it needs
+               to be, we let the TreeViewColumn take care of making them all
+               line up.
+               """
+               box_size = self.box_size(widget)
+
+               cols = self.node[0]
+               for start, end, colour in self.in_lines + self.out_lines:
+                       cols = int(max(cols, start, end))
+
+               (column, colour, names) = self.node
+               names_len = 0
+               if (len(names) != 0):
+                       for item in names:
+                               names_len += len(item)
+
+               width = box_size * (cols + 1 ) + names_len
+               height = box_size
+
+               # FIXME I have no idea how to use cell_area properly
+               return (0, 0, width, height)
+
+       def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
+               """Render an individual cell.
+
+               Draws the cell contents using cairo, taking care to clip what we
+               do to within the background area so we don't draw over other cells.
+               Note that we're a bit naughty there and should really be drawing
+               in the cell_area (or even the exposed area), but we explicitly don't
+               want any gutter.
+
+               We try and be a little clever, if the line we need to draw is going
+               to cross other columns we actually draw it as in the .---' style
+               instead of a pure diagonal ... this reduces confusion by an
+               incredible amount.
+               """
+               ctx = window.cairo_create()
+               ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
+               ctx.clip()
+
+               box_size = self.box_size(widget)
+
+               ctx.set_line_width(box_size / 8)
+               ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
+
+               # Draw lines into the cell
+               for start, end, colour in self.in_lines:
+                       ctx.move_to(cell_area.x + box_size * start + box_size / 2,
+                                       bg_area.y - bg_area.height / 2)
+
+                       if start - end > 1:
+                               ctx.line_to(cell_area.x + box_size * start, bg_area.y)
+                               ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y)
+                       elif start - end < -1:
+                               ctx.line_to(cell_area.x + box_size * start + box_size,
+                                               bg_area.y)
+                               ctx.line_to(cell_area.x + box_size * end, bg_area.y)
+
+                       ctx.line_to(cell_area.x + box_size * end + box_size / 2,
+                                       bg_area.y + bg_area.height / 2)
+
+                       self.set_colour(ctx, colour, 0.0, 0.65)
+                       ctx.stroke()
+
+               # Draw lines out of the cell
+               for start, end, colour in self.out_lines:
+                       ctx.move_to(cell_area.x + box_size * start + box_size / 2,
+                                       bg_area.y + bg_area.height / 2)
+
+                       if start - end > 1:
+                               ctx.line_to(cell_area.x + box_size * start,
+                                               bg_area.y + bg_area.height)
+                               ctx.line_to(cell_area.x + box_size * end + box_size,
+                                               bg_area.y + bg_area.height)
+                       elif start - end < -1:
+                               ctx.line_to(cell_area.x + box_size * start + box_size,
+                                               bg_area.y + bg_area.height)
+                               ctx.line_to(cell_area.x + box_size * end,
+                                               bg_area.y + bg_area.height)
+
+                       ctx.line_to(cell_area.x + box_size * end + box_size / 2,
+                                       bg_area.y + bg_area.height / 2 + bg_area.height)
+
+                       self.set_colour(ctx, colour, 0.0, 0.65)
+                       ctx.stroke()
+
+               # Draw the revision node in the right column
+               (column, colour, names) = self.node
+               ctx.arc(cell_area.x + box_size * column + box_size / 2,
+                               cell_area.y + cell_area.height / 2,
+                               box_size / 4, 0, 2 * math.pi)
+
+
+               self.set_colour(ctx, colour, 0.0, 0.5)
+               ctx.stroke_preserve()
+
+               self.set_colour(ctx, colour, 0.5, 1.0)
+               ctx.fill_preserve()
+
+               if (len(names) != 0):
+                       name = " "
+                       for item in names:
+                               name = name + item + " "
+
+                       ctx.set_font_size(13)
+                       if (flags & 1):
+                               self.set_colour(ctx, colour, 0.5, 1.0)
+                       else:
+                               self.set_colour(ctx, colour, 0.0, 0.5)
+                       ctx.show_text(name)
+
+class Commit:
+       """ This represent a commit object obtained after parsing the git-rev-list
+       output """
+
+       children_sha1 = {}
+
+       def __init__(self, commit_lines):
+               self.message            = ""
+               self.author             = ""
+               self.date               = ""
+               self.committer          = ""
+               self.commit_date        = ""
+               self.commit_sha1        = ""
+               self.parent_sha1        = [ ]
+               self.parse_commit(commit_lines)
+
+
+       def parse_commit(self, commit_lines):
+
+               # First line is the sha1 lines
+               line = string.strip(commit_lines[0])
+               sha1 = re.split(" ", line)
+               self.commit_sha1 = sha1[0]
+               self.parent_sha1 = sha1[1:]
+
+               #build the child list
+               for parent_id in self.parent_sha1:
+                       try:
+                               Commit.children_sha1[parent_id].append(self.commit_sha1)
+                       except KeyError:
+                               Commit.children_sha1[parent_id] = [self.commit_sha1]
+
+               # IF we don't have parent
+               if (len(self.parent_sha1) == 0):
+                       self.parent_sha1 = [0]
+
+               for line in commit_lines[1:]:
+                       m = re.match("^ ", line)
+                       if (m != None):
+                               # First line of the commit message used for short log
+                               if self.message == "":
+                                       self.message = string.strip(line)
+                               continue
+
+                       m = re.match("tree", line)
+                       if (m != None):
+                               continue
+
+                       m = re.match("parent", line)
+                       if (m != None):
+                               continue
+
+                       m = re_ident.match(line)
+                       if (m != None):
+                               date = show_date(m.group('epoch'), m.group('tz'))
+                               if m.group(1) == "author":
+                                       self.author = m.group('ident')
+                                       self.date = date
+                               elif m.group(1) == "committer":
+                                       self.committer = m.group('ident')
+                                       self.commit_date = date
+
+                               continue
+
+       def get_message(self, with_diff=0):
+               if (with_diff == 1):
+                       message = self.diff_tree()
+               else:
+                       fp = os.popen("git cat-file commit " + self.commit_sha1)
+                       message = fp.read()
+                       fp.close()
+
+               return message
+
+       def diff_tree(self):
+               fp = os.popen("git diff-tree --pretty --cc  -v -p --always " +  self.commit_sha1)
+               diff = fp.read()
+               fp.close()
+               return diff
+
+class DiffWindow:
+       """Diff window.
+       This object represents and manages a single window containing the
+       differences between two revisions on a branch.
+       """
+
+       def __init__(self):
+               self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+               self.window.set_border_width(0)
+               self.window.set_title("Git repository browser diff window")
+
+               # Use two thirds of the screen by default
+               screen = self.window.get_screen()
+               monitor = screen.get_monitor_geometry(0)
+               width = int(monitor.width * 0.66)
+               height = int(monitor.height * 0.66)
+               self.window.set_default_size(width, height)
+
+               self.construct()
+
+       def construct(self):
+               """Construct the window contents."""
+               vbox = gtk.VBox()
+               self.window.add(vbox)
+               vbox.show()
+
+               menu_bar = gtk.MenuBar()
+               save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
+               save_menu.connect("activate", self.save_menu_response, "save")
+               save_menu.show()
+               menu_bar.append(save_menu)
+               vbox.pack_start(menu_bar, expand=False, fill=True)
+               menu_bar.show()
+
+               scrollwin = gtk.ScrolledWindow()
+               scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+               scrollwin.set_shadow_type(gtk.SHADOW_IN)
+               vbox.pack_start(scrollwin, expand=True, fill=True)
+               scrollwin.show()
+
+               if have_gtksourceview:
+                       self.buffer = gtksourceview.SourceBuffer()
+                       slm = gtksourceview.SourceLanguagesManager()
+                       gsl = slm.get_language_from_mime_type("text/x-patch")
+                       self.buffer.set_highlight(True)
+                       self.buffer.set_language(gsl)
+                       sourceview = gtksourceview.SourceView(self.buffer)
+               else:
+                       self.buffer = gtk.TextBuffer()
+                       sourceview = gtk.TextView(self.buffer)
+
+               sourceview.set_editable(False)
+               sourceview.modify_font(pango.FontDescription("Monospace"))
+               scrollwin.add(sourceview)
+               sourceview.show()
+
+
+       def set_diff(self, commit_sha1, parent_sha1, encoding):
+               """Set the differences showed by this window.
+               Compares the two trees and populates the window with the
+               differences.
+               """
+               # Diff with the first commit or the last commit shows nothing
+               if (commit_sha1 == 0 or parent_sha1 == 0 ):
+                       return
+
+               fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
+               self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
+               fp.close()
+               self.window.show()
+
+       def save_menu_response(self, widget, string):
+               dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
+                               (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+                                       gtk.STOCK_SAVE, gtk.RESPONSE_OK))
+               dialog.set_default_response(gtk.RESPONSE_OK)
+               response = dialog.run()
+               if response == gtk.RESPONSE_OK:
+                       patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
+                                       self.buffer.get_end_iter())
+                       fp = open(dialog.get_filename(), "w")
+                       fp.write(patch_buffer)
+                       fp.close()
+               dialog.destroy()
+
+class GitView:
+       """ This is the main class
+       """
+       version = "0.7"
+
+       def __init__(self, with_diff=0):
+               self.with_diff = with_diff
+               self.window =   gtk.Window(gtk.WINDOW_TOPLEVEL)
+               self.window.set_border_width(0)
+               self.window.set_title("Git repository browser")
+
+               self.get_encoding()
+               self.get_bt_sha1()
+
+               # Use three-quarters of the screen by default
+               screen = self.window.get_screen()
+               monitor = screen.get_monitor_geometry(0)
+               width = int(monitor.width * 0.75)
+               height = int(monitor.height * 0.75)
+               self.window.set_default_size(width, height)
+
+               # FIXME AndyFitz!
+               icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
+               self.window.set_icon(icon)
+
+               self.accel_group = gtk.AccelGroup()
+               self.window.add_accel_group(self.accel_group)
+
+               self.construct()
+
+       def get_bt_sha1(self):
+               """ Update the bt_sha1 dictionary with the
+               respective sha1 details """
+
+               self.bt_sha1 = { }
+               ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
+               fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
+               while 1:
+                       line = string.strip(fp.readline())
+                       if line == '':
+                               break
+                       m = ls_remote.match(line)
+                       if not m:
+                               continue
+                       (sha1, name) = (m.group(1), m.group(2))
+                       if not self.bt_sha1.has_key(sha1):
+                               self.bt_sha1[sha1] = []
+                       self.bt_sha1[sha1].append(name)
+               fp.close()
+
+       def get_encoding(self):
+               fp = os.popen("git repo-config --get i18n.commitencoding")
+               self.encoding=string.strip(fp.readline())
+               fp.close()
+               if (self.encoding == ""):
+                       self.encoding = "utf-8"
+
+
+       def construct(self):
+               """Construct the window contents."""
+               vbox = gtk.VBox()
+               paned = gtk.VPaned()
+               paned.pack1(self.construct_top(), resize=False, shrink=True)
+               paned.pack2(self.construct_bottom(), resize=False, shrink=True)
+               menu_bar = gtk.MenuBar()
+               menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
+               help_menu = gtk.MenuItem("Help")
+               menu = gtk.Menu()
+               about_menu = gtk.MenuItem("About")
+               menu.append(about_menu)
+               about_menu.connect("activate", self.about_menu_response, "about")
+               about_menu.show()
+               help_menu.set_submenu(menu)
+               help_menu.show()
+               menu_bar.append(help_menu)
+               menu_bar.show()
+               vbox.pack_start(menu_bar, expand=False, fill=True)
+               vbox.pack_start(paned, expand=True, fill=True)
+               self.window.add(vbox)
+               paned.show()
+               vbox.show()
+
+
+       def construct_top(self):
+               """Construct the top-half of the window."""
+               vbox = gtk.VBox(spacing=6)
+               vbox.set_border_width(12)
+               vbox.show()
+
+
+               scrollwin = gtk.ScrolledWindow()
+               scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+               scrollwin.set_shadow_type(gtk.SHADOW_IN)
+               vbox.pack_start(scrollwin, expand=True, fill=True)
+               scrollwin.show()
+
+               self.treeview = gtk.TreeView()
+               self.treeview.set_rules_hint(True)
+               self.treeview.set_search_column(4)
+               self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
+               scrollwin.add(self.treeview)
+               self.treeview.show()
+
+               cell = CellRendererGraph()
+               column = gtk.TreeViewColumn()
+               column.set_resizable(True)
+               column.pack_start(cell, expand=True)
+               column.add_attribute(cell, "node", 1)
+               column.add_attribute(cell, "in-lines", 2)
+               column.add_attribute(cell, "out-lines", 3)
+               self.treeview.append_column(column)
+
+               cell = gtk.CellRendererText()
+               cell.set_property("width-chars", 65)
+               cell.set_property("ellipsize", pango.ELLIPSIZE_END)
+               column = gtk.TreeViewColumn("Message")
+               column.set_resizable(True)
+               column.pack_start(cell, expand=True)
+               column.add_attribute(cell, "text", 4)
+               self.treeview.append_column(column)
+
+               cell = gtk.CellRendererText()
+               cell.set_property("width-chars", 40)
+               cell.set_property("ellipsize", pango.ELLIPSIZE_END)
+               column = gtk.TreeViewColumn("Author")
+               column.set_resizable(True)
+               column.pack_start(cell, expand=True)
+               column.add_attribute(cell, "text", 5)
+               self.treeview.append_column(column)
+
+               cell = gtk.CellRendererText()
+               cell.set_property("ellipsize", pango.ELLIPSIZE_END)
+               column = gtk.TreeViewColumn("Date")
+               column.set_resizable(True)
+               column.pack_start(cell, expand=True)
+               column.add_attribute(cell, "text", 6)
+               self.treeview.append_column(column)
+
+               return vbox
+
+       def about_menu_response(self, widget, string):
+               dialog = gtk.AboutDialog()
+               dialog.set_name("Gitview")
+               dialog.set_version(GitView.version)
+               dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
+               dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
+               dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
+               dialog.set_wrap_license(True)
+               dialog.run()
+               dialog.destroy()
+
+
+       def construct_bottom(self):
+               """Construct the bottom half of the window."""
+               vbox = gtk.VBox(False, spacing=6)
+               vbox.set_border_width(12)
+               (width, height) = self.window.get_size()
+               vbox.set_size_request(width, int(height / 2.5))
+               vbox.show()
+
+               self.table = gtk.Table(rows=4, columns=4)
+               self.table.set_row_spacings(6)
+               self.table.set_col_spacings(6)
+               vbox.pack_start(self.table, expand=False, fill=True)
+               self.table.show()
+
+               align = gtk.Alignment(0.0, 0.5)
+               label = gtk.Label()
+               label.set_markup("<b>Revision:</b>")
+               align.add(label)
+               self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
+               label.show()
+               align.show()
+
+               align = gtk.Alignment(0.0, 0.5)
+               self.revid_label = gtk.Label()
+               self.revid_label.set_selectable(True)
+               align.add(self.revid_label)
+               self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
+               self.revid_label.show()
+               align.show()
+
+               align = gtk.Alignment(0.0, 0.5)
+               label = gtk.Label()
+               label.set_markup("<b>Committer:</b>")
+               align.add(label)
+               self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
+               label.show()
+               align.show()
+
+               align = gtk.Alignment(0.0, 0.5)
+               self.committer_label = gtk.Label()
+               self.committer_label.set_selectable(True)
+               align.add(self.committer_label)
+               self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
+               self.committer_label.show()
+               align.show()
+
+               align = gtk.Alignment(0.0, 0.5)
+               label = gtk.Label()
+               label.set_markup("<b>Timestamp:</b>")
+               align.add(label)
+               self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
+               label.show()
+               align.show()
+
+               align = gtk.Alignment(0.0, 0.5)
+               self.timestamp_label = gtk.Label()
+               self.timestamp_label.set_selectable(True)
+               align.add(self.timestamp_label)
+               self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
+               self.timestamp_label.show()
+               align.show()
+
+               align = gtk.Alignment(0.0, 0.5)
+               label = gtk.Label()
+               label.set_markup("<b>Parents:</b>")
+               align.add(label)
+               self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
+               label.show()
+               align.show()
+               self.parents_widgets = []
+
+               align = gtk.Alignment(0.0, 0.5)
+               label = gtk.Label()
+               label.set_markup("<b>Children:</b>")
+               align.add(label)
+               self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
+               label.show()
+               align.show()
+               self.children_widgets = []
+
+               scrollwin = gtk.ScrolledWindow()
+               scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+               scrollwin.set_shadow_type(gtk.SHADOW_IN)
+               vbox.pack_start(scrollwin, expand=True, fill=True)
+               scrollwin.show()
+
+               if have_gtksourceview:
+                       self.message_buffer = gtksourceview.SourceBuffer()
+                       slm = gtksourceview.SourceLanguagesManager()
+                       gsl = slm.get_language_from_mime_type("text/x-patch")
+                       self.message_buffer.set_highlight(True)
+                       self.message_buffer.set_language(gsl)
+                       sourceview = gtksourceview.SourceView(self.message_buffer)
+               else:
+                       self.message_buffer = gtk.TextBuffer()
+                       sourceview = gtk.TextView(self.message_buffer)
+
+               sourceview.set_editable(False)
+               sourceview.modify_font(pango.FontDescription("Monospace"))
+               scrollwin.add(sourceview)
+               sourceview.show()
+
+               return vbox
+
+       def _treeview_cursor_cb(self, *args):
+               """Callback for when the treeview cursor changes."""
+               (path, col) = self.treeview.get_cursor()
+               commit = self.model[path][0]
+
+               if commit.committer is not None:
+                       committer = commit.committer
+                       timestamp = commit.commit_date
+                       message   =  commit.get_message(self.with_diff)
+                       revid_label = commit.commit_sha1
+               else:
+                       committer = ""
+                       timestamp = ""
+                       message = ""
+                       revid_label = ""
+
+               self.revid_label.set_text(revid_label)
+               self.committer_label.set_text(committer)
+               self.timestamp_label.set_text(timestamp)
+               self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
+
+               for widget in self.parents_widgets:
+                       self.table.remove(widget)
+
+               self.parents_widgets = []
+               self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
+               for idx, parent_id in enumerate(commit.parent_sha1):
+                       self.table.set_row_spacing(idx + 3, 0)
+
+                       align = gtk.Alignment(0.0, 0.0)
+                       self.parents_widgets.append(align)
+                       self.table.attach(align, 1, 2, idx + 3, idx + 4,
+                                       gtk.EXPAND | gtk.FILL, gtk.FILL)
+                       align.show()
+
+                       hbox = gtk.HBox(False, 0)
+                       align.add(hbox)
+                       hbox.show()
+
+                       label = gtk.Label(parent_id)
+                       label.set_selectable(True)
+                       hbox.pack_start(label, expand=False, fill=True)
+                       label.show()
+
+                       image = gtk.Image()
+                       image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
+                       image.show()
+
+                       button = gtk.Button()
+                       button.add(image)
+                       button.set_relief(gtk.RELIEF_NONE)
+                       button.connect("clicked", self._go_clicked_cb, parent_id)
+                       hbox.pack_start(button, expand=False, fill=True)
+                       button.show()
+
+                       image = gtk.Image()
+                       image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
+                       image.show()
+
+                       button = gtk.Button()
+                       button.add(image)
+                       button.set_relief(gtk.RELIEF_NONE)
+                       button.set_sensitive(True)
+                       button.connect("clicked", self._show_clicked_cb,
+                                       commit.commit_sha1, parent_id, self.encoding)
+                       hbox.pack_start(button, expand=False, fill=True)
+                       button.show()
+
+               # Populate with child details
+               for widget in self.children_widgets:
+                       self.table.remove(widget)
+
+               self.children_widgets = []
+               try:
+                       child_sha1 = Commit.children_sha1[commit.commit_sha1]
+               except KeyError:
+                       # We don't have child
+                       child_sha1 = [ 0 ]
+
+               if ( len(child_sha1) > len(commit.parent_sha1)):
+                       self.table.resize(4 + len(child_sha1) - 1, 4)
+
+               for idx, child_id in enumerate(child_sha1):
+                       self.table.set_row_spacing(idx + 3, 0)
+
+                       align = gtk.Alignment(0.0, 0.0)
+                       self.children_widgets.append(align)
+                       self.table.attach(align, 3, 4, idx + 3, idx + 4,
+                                       gtk.EXPAND | gtk.FILL, gtk.FILL)
+                       align.show()
+
+                       hbox = gtk.HBox(False, 0)
+                       align.add(hbox)
+                       hbox.show()
+
+                       label = gtk.Label(child_id)
+                       label.set_selectable(True)
+                       hbox.pack_start(label, expand=False, fill=True)
+                       label.show()
+
+                       image = gtk.Image()
+                       image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
+                       image.show()
+
+                       button = gtk.Button()
+                       button.add(image)
+                       button.set_relief(gtk.RELIEF_NONE)
+                       button.connect("clicked", self._go_clicked_cb, child_id)
+                       hbox.pack_start(button, expand=False, fill=True)
+                       button.show()
+
+                       image = gtk.Image()
+                       image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
+                       image.show()
+
+                       button = gtk.Button()
+                       button.add(image)
+                       button.set_relief(gtk.RELIEF_NONE)
+                       button.set_sensitive(True)
+                       button.connect("clicked", self._show_clicked_cb,
+                                       child_id, commit.commit_sha1)
+                       hbox.pack_start(button, expand=False, fill=True)
+                       button.show()
+
+       def _destroy_cb(self, widget):
+               """Callback for when a window we manage is destroyed."""
+               self.quit()
+
+
+       def quit(self):
+               """Stop the GTK+ main loop."""
+               gtk.main_quit()
+
+       def run(self, args):
+               self.set_branch(args)
+               self.window.connect("destroy", self._destroy_cb)
+               self.window.show()
+               gtk.main()
+
+       def set_branch(self, args):
+               """Fill in different windows with info from the reposiroty"""
+               fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
+               git_rev_list_cmd = fp.read()
+               fp.close()
+               fp = os.popen("git rev-list  --header --topo-order --parents " + git_rev_list_cmd)
+               self.update_window(fp)
+
+       def update_window(self, fp):
+               commit_lines = []
+
+               self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
+                               gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
+
+               # used for cursor positioning
+               self.index = {}
+
+               self.colours = {}
+               self.nodepos = {}
+               self.incomplete_line = {}
+               self.commits = []
+
+               index = 0
+               last_colour = 0
+               last_nodepos = -1
+               out_line = []
+               input_line = fp.readline()
+               while (input_line != ""):
+                       # The commit header ends with '\0'
+                       # This NULL is immediately followed by the sha1 of the
+                       # next commit
+                       if (input_line[0] != '\0'):
+                               commit_lines.append(input_line)
+                               input_line = fp.readline()
+                               continue;
+
+                       commit = Commit(commit_lines)
+                       if (commit != None ):
+                               self.commits.append(commit)
+
+                       # Skip the '\0
+                       commit_lines = []
+                       commit_lines.append(input_line[1:])
+                       input_line = fp.readline()
+
+               fp.close()
+
+               for commit in self.commits:
+                       (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
+                                                                               index, out_line,
+                                                                               last_colour,
+                                                                               last_nodepos)
+                       self.index[commit.commit_sha1] = index
+                       index += 1
+
+               self.treeview.set_model(self.model)
+               self.treeview.show()
+
+       def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
+               in_line=[]
+
+               #   |   -> outline
+               #   X
+               #   |\  <- inline
+
+               # Reset nodepostion
+               if (last_nodepos > 5):
+                       last_nodepos = -1
+
+               # Add the incomplete lines of the last cell in this
+               try:
+                       colour = self.colours[commit.commit_sha1]
+               except KeyError:
+                       self.colours[commit.commit_sha1] = last_colour+1
+                       last_colour = self.colours[commit.commit_sha1]
+                       colour =   self.colours[commit.commit_sha1]
+
+               try:
+                       node_pos = self.nodepos[commit.commit_sha1]
+               except KeyError:
+                       self.nodepos[commit.commit_sha1] = last_nodepos+1
+                       last_nodepos = self.nodepos[commit.commit_sha1]
+                       node_pos =  self.nodepos[commit.commit_sha1]
+
+               #The first parent always continue on the same line
+               try:
+                       # check we alreay have the value
+                       tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
+               except KeyError:
+                       self.colours[commit.parent_sha1[0]] = colour
+                       self.nodepos[commit.parent_sha1[0]] = node_pos
+
+               for sha1 in self.incomplete_line.keys():
+                       if (sha1 != commit.commit_sha1):
+                               self.draw_incomplete_line(sha1, node_pos,
+                                               out_line, in_line, index)
+                       else:
+                               del self.incomplete_line[sha1]
+
+
+               for parent_id in commit.parent_sha1:
+                       try:
+                               tmp_node_pos = self.nodepos[parent_id]
+                       except KeyError:
+                               self.colours[parent_id] = last_colour+1
+                               last_colour = self.colours[parent_id]
+                               self.nodepos[parent_id] = last_nodepos+1
+                               last_nodepos = self.nodepos[parent_id]
+
+                       in_line.append((node_pos, self.nodepos[parent_id],
+                                               self.colours[parent_id]))
+                       self.add_incomplete_line(parent_id)
+
+               try:
+                       branch_tag = self.bt_sha1[commit.commit_sha1]
+               except KeyError:
+                       branch_tag = [ ]
+
+
+               node = (node_pos, colour, branch_tag)
+
+               self.model.append([commit, node, out_line, in_line,
+                               commit.message, commit.author, commit.date])
+
+               return (in_line, last_colour, last_nodepos)
+
+       def add_incomplete_line(self, sha1):
+               try:
+                       self.incomplete_line[sha1].append(self.nodepos[sha1])
+               except KeyError:
+                       self.incomplete_line[sha1] = [self.nodepos[sha1]]
+
+       def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
+               for idx, pos in enumerate(self.incomplete_line[sha1]):
+                       if(pos == node_pos):
+                               #remove the straight line and add a slash
+                               if ((pos, pos, self.colours[sha1]) in out_line):
+                                       out_line.remove((pos, pos, self.colours[sha1]))
+                               out_line.append((pos, pos+0.5, self.colours[sha1]))
+                               self.incomplete_line[sha1][idx] = pos = pos+0.5
+                       try:
+                               next_commit = self.commits[index+1]
+                               if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
+                               # join the line back to the node point
+                               # This need to be done only if we modified it
+                                       in_line.append((pos, pos-0.5, self.colours[sha1]))
+                                       continue;
+                       except IndexError:
+                               pass
+                       in_line.append((pos, pos, self.colours[sha1]))
+
+
+       def _go_clicked_cb(self, widget, revid):
+               """Callback for when the go button for a parent is clicked."""
+               try:
+                       self.treeview.set_cursor(self.index[revid])
+               except KeyError:
+                       print "Revision %s not present in the list" % revid
+                       # revid == 0 is the parent of the first commit
+                       if (revid != 0 ):
+                               print "Try running gitview without any options"
+
+               self.treeview.grab_focus()
+
+       def _show_clicked_cb(self, widget,  commit_sha1, parent_sha1, encoding):
+               """Callback for when the show button for a parent is clicked."""
+               window = DiffWindow()
+               window.set_diff(commit_sha1, parent_sha1, encoding)
+               self.treeview.grab_focus()
+
+if __name__ == "__main__":
+       without_diff = 0
+
+       if (len(sys.argv) > 1 ):
+               if (sys.argv[1] == "--without-diff"):
+                       without_diff = 1
+
+       view = GitView( without_diff != 1)
+       view.run(sys.argv[without_diff:])
+
+
diff --git a/contrib/gitview/gitview.txt b/contrib/gitview/gitview.txt
new file mode 100644 (file)
index 0000000..fcf759c
--- /dev/null
@@ -0,0 +1,38 @@
+gitview(1)
+==========
+
+NAME
+----
+gitview - A GTK based repository browser for git
+
+SYNOPSIS
+--------
+'gitview'  [options] [args]
+
+DESCRIPTION
+---------
+
+Dependencies
+
+* Python 2.4
+* PyGTK 2.8 or later
+* PyCairo 1.0 or later
+
+OPTIONS
+------
+       --without-diff
+               If the user doesn't want to list the commit diffs in the main window. This may speed up the repository browsing.
+
+       <args>
+               All the valid option for git-rev-list(1)
+
+EXAMPLES
+------
+       gitview v2.6.12.. include/scsi drivers/scsi
+         Show as the changes since version v2.6.12 that changed any file in the include/scsi
+         or drivers/scsi subdirectories
+
+       gitview --since=2.weeks.ago
+         Show the changes during the last two weeks 
+
+       
index c2f656a..2ed5984 100644 (file)
@@ -19,8 +19,9 @@
  */
 
 #include <stdlib.h>
+#include <string.h>
+#include <zlib.h>
 #include "delta.h"
-#include "zlib.h"
 
 
 /* block size: min = 16, max = 64k, power of 2 */
 #define MIN(a, b) ((a) < (b) ? (a) : (b))
 
 #define GR_PRIME 0x9e370001
-#define HASH(v, b) (((unsigned int)(v) * GR_PRIME) >> (32 - (b)))
-       
-static unsigned int hashbits(unsigned int size)
-{
-       unsigned int val = 1, bits = 0;
-       while (val < size && bits < 32) {
-               val <<= 1;
-               bits++;
-       }
-       return bits ? bits: 1;
-}
-
-typedef struct s_chanode {
-       struct s_chanode *next;
-       int icurr;
-} chanode_t;
-
-typedef struct s_chastore {
-       int isize, nsize;
-       chanode_t *ancur;
-} chastore_t;
-
-static void cha_init(chastore_t *cha, int isize, int icount)
-{
-       cha->isize = isize;
-       cha->nsize = icount * isize;
-       cha->ancur = NULL;
-}
-
-static void *cha_alloc(chastore_t *cha)
-{
-       chanode_t *ancur;
-       void *data;
+#define HASH(v, shift) (((unsigned int)(v) * GR_PRIME) >> (shift))
 
-       ancur = cha->ancur;
-       if (!ancur || ancur->icurr == cha->nsize) {
-               ancur = malloc(sizeof(chanode_t) + cha->nsize);
-               if (!ancur)
-                       return NULL;
-               ancur->icurr = 0;
-               ancur->next = cha->ancur;
-               cha->ancur = ancur;
-       }
-
-       data = (void *)ancur + sizeof(chanode_t) + ancur->icurr;
-       ancur->icurr += cha->isize;
-       return data;
-}
-
-static void cha_free(chastore_t *cha)
-{
-       chanode_t *cur = cha->ancur;
-       while (cur) {
-               chanode_t *tmp = cur;
-               cur = cur->next;
-               free(tmp);
-       }
-}
-
-typedef struct s_bdrecord {
-       struct s_bdrecord *next;
-       unsigned int fp;
+struct index {
        const unsigned char *ptr;
-} bdrecord_t;
-
-typedef struct s_bdfile {
-       chastore_t cha;
-       unsigned int fphbits;
-       bdrecord_t **fphash;
-} bdfile_t;
+       unsigned int val;
+       struct index *next;
+};
 
-static int delta_prepare(const unsigned char *buf, int bufsize, bdfile_t *bdf)
+static struct index ** delta_index(const unsigned char *buf,
+                                  unsigned long bufsize,
+                                  unsigned int *hash_shift)
 {
-       unsigned int fphbits;
-       int i, hsize;
-       const unsigned char *data, *top;
-       bdrecord_t *brec;
-       bdrecord_t **fphash;
-
-       fphbits = hashbits(bufsize / BLK_SIZE + 1);
-       hsize = 1 << fphbits;
-       fphash = malloc(hsize * sizeof(bdrecord_t *));
-       if (!fphash)
-               return -1;
-       for (i = 0; i < hsize; i++)
-               fphash[i] = NULL;
-       cha_init(&bdf->cha, sizeof(bdrecord_t), hsize / 4 + 1);
-
-       top = buf + bufsize;
-       data = buf + (bufsize / BLK_SIZE) * BLK_SIZE;
-       if (data == top)
+       unsigned int hsize, hshift, entries, blksize, i;
+       const unsigned char *data;
+       struct index *entry, **hash;
+       void *mem;
+
+       /* determine index hash size */
+       entries = (bufsize + BLK_SIZE - 1) / BLK_SIZE;
+       hsize = entries / 4;
+       for (i = 4; (1 << i) < hsize && i < 16; i++);
+       hsize = 1 << i;
+       hshift = 32 - i;
+       *hash_shift = hshift;
+
+       /* allocate lookup index */
+       mem = malloc(hsize * sizeof(*hash) + entries * sizeof(*entry));
+       if (!mem)
+               return NULL;
+       hash = mem;
+       entry = mem + hsize * sizeof(*hash);
+       memset(hash, 0, hsize * sizeof(*hash));
+
+       /* then populate it */
+       data = buf + entries * BLK_SIZE - BLK_SIZE;
+       blksize = bufsize - (data - buf);
+       while (data >= buf) {
+               unsigned int val = adler32(0, data, blksize);
+               i = HASH(val, hshift);
+               entry->ptr = data;
+               entry->val = val;
+               entry->next = hash[i];
+               hash[i] = entry++;
+               blksize = BLK_SIZE;
                data -= BLK_SIZE;
+       }
 
-       for ( ; data >= buf; data -= BLK_SIZE) {
-               brec = cha_alloc(&bdf->cha);
-               if (!brec) {
-                       cha_free(&bdf->cha);
-                       free(fphash);
-                       return -1;
-               }
-               brec->fp = adler32(0, data, MIN(BLK_SIZE, top - data));
-               brec->ptr = data;
-               i = HASH(brec->fp, fphbits);
-               brec->next = fphash[i];
-               fphash[i] = brec;
-       }
-
-       bdf->fphbits = fphbits;
-       bdf->fphash = fphash;
-
-       return 0;
-}
-
-static void delta_cleanup(bdfile_t *bdf)
-{
-       free(bdf->fphash);
-       cha_free(&bdf->cha);
+       return hash;
 }
 
+/* provide the size of the copy opcode given the block offset and size */
 #define COPYOP_SIZE(o, s) \
     (!!(o & 0xff) + !!(o & 0xff00) + !!(o & 0xff0000) + !!(o & 0xff000000) + \
      !!(s & 0xff) + !!(s & 0xff00) + 1)
 
+/* the maximum size for any opcode */
+#define MAX_OP_SIZE COPYOP_SIZE(0xffffffff, 0xffffffff)
+
 void *diff_delta(void *from_buf, unsigned long from_size,
                 void *to_buf, unsigned long to_size,
                 unsigned long *delta_size,
                 unsigned long max_size)
 {
-       int i, outpos, outsize, inscnt, csize, msize, moff;
-       unsigned int fp;
-       const unsigned char *ref_data, *ref_top, *data, *top, *ptr1, *ptr2;
-       unsigned char *out, *orig;
-       bdrecord_t *brec;
-       bdfile_t bdf;
+       unsigned int i, outpos, outsize, inscnt, hash_shift;
+       const unsigned char *ref_data, *ref_top, *data, *top;
+       unsigned char *out;
+       struct index *entry, **hash;
 
-       if (!from_size || !to_size || delta_prepare(from_buf, from_size, &bdf))
+       if (!from_size || !to_size)
+               return NULL;
+       hash = delta_index(from_buf, from_size, &hash_shift);
+       if (!hash)
                return NULL;
-       
+
        outpos = 0;
        outsize = 8192;
+       if (max_size && outsize >= max_size)
+               outsize = max_size + MAX_OP_SIZE + 1;
        out = malloc(outsize);
        if (!out) {
-               delta_cleanup(&bdf);
+               free(hash);
                return NULL;
        }
 
@@ -199,28 +138,32 @@ void *diff_delta(void *from_buf, unsigned long from_size,
        }
 
        inscnt = 0;
-       moff = 0;
-       while (data < top) {
-               msize = 0;
-               fp = adler32(0, data, MIN(top - data, BLK_SIZE));
-               i = HASH(fp, bdf.fphbits);
-               for (brec = bdf.fphash[i]; brec; brec = brec->next) {
-                       if (brec->fp == fp) {
-                               csize = ref_top - brec->ptr;
-                               if (csize > top - data)
-                                       csize = top - data;
-                               for (ptr1 = brec->ptr, ptr2 = data; 
-                                    csize && *ptr1 == *ptr2;
-                                    csize--, ptr1++, ptr2++);
 
-                               csize = ptr1 - brec->ptr;
-                               if (csize > msize) {
-                                       moff = brec->ptr - ref_data;
-                                       msize = csize;
-                                       if (msize >= 0x10000) {
-                                               msize = 0x10000;
-                                               break;
-                                       }
+       while (data < top) {
+               unsigned int moff = 0, msize = 0;
+               unsigned int blksize = MIN(top - data, BLK_SIZE);
+               unsigned int val = adler32(0, data, blksize);
+               i = HASH(val, hash_shift);
+               for (entry = hash[i]; entry; entry = entry->next) {
+                       const unsigned char *ref = entry->ptr;
+                       const unsigned char *src = data;
+                       unsigned int ref_size = ref_top - ref;
+                       if (entry->val != val)
+                               continue;
+                       if (ref_size > top - src)
+                               ref_size = top - src;
+                       while (ref_size && *src++ == *ref) {
+                               ref++;
+                               ref_size--;
+                       }
+                       ref_size = ref - entry->ptr;
+                       if (ref_size > msize) {
+                               /* this is our best match so far */
+                               moff = entry->ptr - ref_data;
+                               msize = ref_size;
+                               if (msize >= 0x10000) {
+                                       msize = 0x10000;
+                                       break;
                                }
                        }
                }
@@ -235,13 +178,15 @@ void *diff_delta(void *from_buf, unsigned long from_size,
                                inscnt = 0;
                        }
                } else {
+                       unsigned char *op;
+
                        if (inscnt) {
                                out[outpos - inscnt - 1] = inscnt;
                                inscnt = 0;
                        }
 
                        data += msize;
-                       orig = out + outpos++;
+                       op = out + outpos++;
                        i = 0x80;
 
                        if (moff & 0xff) { out[outpos++] = moff; i |= 0x01; }
@@ -256,23 +201,21 @@ void *diff_delta(void *from_buf, unsigned long from_size,
                        msize >>= 8;
                        if (msize & 0xff) { out[outpos++] = msize; i |= 0x20; }
 
-                       *orig = i;
-               }
-
-               if (max_size && outpos > max_size) {
-                       free(out);
-                       delta_cleanup(&bdf);
-                       return NULL;
+                       *op = i;
                }
 
-               /* next time around the largest possible output is 1 + 4 + 3 */
-               if (outpos > outsize - 8) {
+               if (outpos >= outsize - MAX_OP_SIZE) {
                        void *tmp = out;
                        outsize = outsize * 3 / 2;
-                       out = realloc(out, outsize);
+                       if (max_size && outsize >= max_size)
+                               outsize = max_size + MAX_OP_SIZE + 1;
+                       if (max_size && outpos > max_size)
+                               out = NULL;
+                       else
+                               out = realloc(out, outsize);
                        if (!out) {
                                free(tmp);
-                               delta_cleanup(&bdf);
+                               free(hash);
                                return NULL;
                        }
                }
@@ -281,7 +224,7 @@ void *diff_delta(void *from_buf, unsigned long from_size,
        if (inscnt)
                out[outpos - inscnt - 1] = inscnt;
 
-       delta_cleanup(&bdf);
+       free(hash);
        *delta_size = outpos;
        return out;
 }
index 0c3f800..b1c05b3 100644 (file)
@@ -193,7 +193,7 @@ int main(int argc, const char **argv)
                        show_file('-', ce);
                        continue;
                }
-               changed = ce_match_stat(ce, &st);
+               changed = ce_match_stat(ce, &st, 0);
                if (!changed && !diff_options.find_copies_harder)
                        continue;
                oldmode = ntohl(ce->ce_mode);
index f8a102e..12a9418 100644 (file)
@@ -33,7 +33,7 @@ static int get_stat_data(struct cache_entry *ce,
                        }
                        return -1;
                }
-               changed = ce_match_stat(ce, &st);
+               changed = ce_match_stat(ce, &st, 0);
                if (changed) {
                        mode = create_ce_mode(st.st_mode);
                        if (!trust_executable_bit &&
diff --git a/diff.c b/diff.c
index 890bdaa..c0548ee 100644 (file)
--- a/diff.c
+++ b/diff.c
@@ -178,11 +178,12 @@ static void emit_rewrite_diff(const char *name_a,
                copy_file('+', temp[1].name);
 }
 
-static void builtin_diff(const char *name_a,
+static const char *builtin_diff(const char *name_a,
                         const char *name_b,
                         struct diff_tempfile *temp,
                         const char *xfrm_msg,
-                        int complete_rewrite)
+                        int complete_rewrite,
+                        const char **args)
 {
        int i, next_at, cmd_size;
        const char *const diff_cmd = "diff -L%s -L%s";
@@ -242,19 +243,24 @@ static void builtin_diff(const char *name_a,
                }
                if (xfrm_msg && xfrm_msg[0])
                        puts(xfrm_msg);
+               /*
+                * we do not run diff between different kind
+                * of objects.
+                */
                if (strncmp(temp[0].mode, temp[1].mode, 3))
-                       /* we do not run diff between different kind
-                        * of objects.
-                        */
-                       exit(0);
+                       return NULL;
                if (complete_rewrite) {
-                       fflush(NULL);
                        emit_rewrite_diff(name_a, name_b, temp);
-                       exit(0);
+                       return NULL;
                }
        }
-       fflush(NULL);
-       execlp("/bin/sh","sh", "-c", cmd, NULL);
+
+       /* This is disgusting */
+       *args++ = "sh";
+       *args++ = "-c";
+       *args++ = cmd;
+       *args = NULL;
+       return "/bin/sh";
 }
 
 struct diff_filespec *alloc_filespec(const char *path)
@@ -311,7 +317,7 @@ static int work_tree_matches(const char *name, const unsigned char *sha1)
        ce = active_cache[pos];
        if ((lstat(name, &st) < 0) ||
            !S_ISREG(st.st_mode) || /* careful! */
-           ce_match_stat(ce, &st) ||
+           ce_match_stat(ce, &st, 0) ||
            memcmp(sha1, ce->sha1, 20))
                return 0;
        /* we return 1 only when we can stat, it is a regular file,
@@ -559,6 +565,40 @@ static void remove_tempfile_on_signal(int signo)
        raise(signo);
 }
 
+static int spawn_prog(const char *pgm, const char **arg)
+{
+       pid_t pid;
+       int status;
+
+       fflush(NULL);
+       pid = fork();
+       if (pid < 0)
+               die("unable to fork");
+       if (!pid) {
+               execvp(pgm, (char *const*) arg);
+               exit(255);
+       }
+
+       while (waitpid(pid, &status, 0) < 0) {
+               if (errno == EINTR)
+                       continue;
+               return -1;
+       }
+
+       /* Earlier we did not check the exit status because
+        * diff exits non-zero if files are different, and
+        * we are not interested in knowing that.  It was a
+        * mistake which made it harder to quit a diff-*
+        * session that uses the git-apply-patch-script as
+        * the GIT_EXTERNAL_DIFF.  A custom GIT_EXTERNAL_DIFF
+        * should also exit non-zero only when it wants to
+        * abort the entire diff-* session.
+        */
+       if (WIFEXITED(status) && !WEXITSTATUS(status))
+               return 0;
+       return -1;
+}
+
 /* An external diff command takes:
  *
  * diff-cmd name infile1 infile1-sha1 infile1-mode \
@@ -573,9 +613,9 @@ static void run_external_diff(const char *pgm,
                              const char *xfrm_msg,
                              int complete_rewrite)
 {
+       const char *spawn_arg[10];
        struct diff_tempfile *temp = diff_temp;
-       pid_t pid;
-       int status;
+       int retval;
        static int atexit_asked = 0;
        const char *othername;
 
@@ -592,59 +632,41 @@ static void run_external_diff(const char *pgm,
                signal(SIGINT, remove_tempfile_on_signal);
        }
 
-       fflush(NULL);
-       pid = fork();
-       if (pid < 0)
-               die("unable to fork");
-       if (!pid) {
-               if (pgm) {
-                       if (one && two) {
-                               const char *exec_arg[10];
-                               const char **arg = &exec_arg[0];
-                               *arg++ = pgm;
-                               *arg++ = name;
-                               *arg++ = temp[0].name;
-                               *arg++ = temp[0].hex;
-                               *arg++ = temp[0].mode;
-                               *arg++ = temp[1].name;
-                               *arg++ = temp[1].hex;
-                               *arg++ = temp[1].mode;
-                               if (other) {
-                                       *arg++ = other;
-                                       *arg++ = xfrm_msg;
-                               }
-                               *arg = NULL;
-                               execvp(pgm, (char *const*) exec_arg);
+       if (pgm) {
+               const char **arg = &spawn_arg[0];
+               if (one && two) {
+                       *arg++ = pgm;
+                       *arg++ = name;
+                       *arg++ = temp[0].name;
+                       *arg++ = temp[0].hex;
+                       *arg++ = temp[0].mode;
+                       *arg++ = temp[1].name;
+                       *arg++ = temp[1].hex;
+                       *arg++ = temp[1].mode;
+                       if (other) {
+                               *arg++ = other;
+                               *arg++ = xfrm_msg;
                        }
-                       else
-                               execlp(pgm, pgm, name, NULL);
+               } else {
+                       *arg++ = pgm;
+                       *arg++ = name;
                }
-               /*
-                * otherwise we use the built-in one.
-                */
-               if (one && two)
-                       builtin_diff(name, othername, temp, xfrm_msg,
-                                    complete_rewrite);
-               else
+               *arg = NULL;
+       } else {
+               if (one && two) {
+                       pgm = builtin_diff(name, othername, temp, xfrm_msg, complete_rewrite, spawn_arg);
+               } else
                        printf("* Unmerged path %s\n", name);
-               exit(0);
        }
-       if (waitpid(pid, &status, 0) < 0 ||
-           !WIFEXITED(status) || WEXITSTATUS(status)) {
-               /* Earlier we did not check the exit status because
-                * diff exits non-zero if files are different, and
-                * we are not interested in knowing that.  It was a
-                * mistake which made it harder to quit a diff-*
-                * session that uses the git-apply-patch-script as
-                * the GIT_EXTERNAL_DIFF.  A custom GIT_EXTERNAL_DIFF
-                * should also exit non-zero only when it wants to
-                * abort the entire diff-* session.
-                */
-               remove_tempfile();
+
+       retval = 0;
+       if (pgm)
+               retval = spawn_prog(pgm, spawn_arg);
+       remove_tempfile();
+       if (retval) {
                fprintf(stderr, "external diff died, stopping at %s.\n", name);
                exit(1);
        }
-       remove_tempfile();
 }
 
 static void diff_fill_sha1_info(struct diff_filespec *one)
index 95b5eb4..0fc2b86 100644 (file)
@@ -4,8 +4,6 @@
 #include "cache.h"
 #include "diff.h"
 #include "diffcore.h"
-#include "delta.h"
-#include "count-delta.h"
 
 static int should_break(struct diff_filespec *src,
                        struct diff_filespec *dst,
@@ -47,7 +45,6 @@ static int should_break(struct diff_filespec *src,
         * The value we return is 1 if we want the pair to be broken,
         * or 0 if we do not.
         */
-       void *delta;
        unsigned long delta_size, base_size, src_copied, literal_added;
        int to_break = 0;
 
@@ -69,19 +66,11 @@ static int should_break(struct diff_filespec *src,
        if (base_size < MINIMUM_BREAK_SIZE)
                return 0; /* we do not break too small filepair */
 
-       delta = diff_delta(src->data, src->size,
-                          dst->data, dst->size,
-                          &delta_size, 0);
-       if (!delta)
-               return 0; /* error but caught downstream */
-
-       /* Estimate the edit size by interpreting delta. */
-       if (count_delta(delta, delta_size,
-                       &src_copied, &literal_added)) {
-               free(delta);
-               return 0; /* we cannot tell */
-       }
-       free(delta);
+       if (diffcore_count_changes(src->data, src->size,
+                                  dst->data, dst->size,
+                                  0,
+                                  &src_copied, &literal_added))
+               return 0;
 
        /* Compute merge-score, which is "how much is removed
         * from the source material".  The clean-up stage will
diff --git a/diffcore-delta.c b/diffcore-delta.c
new file mode 100644 (file)
index 0000000..1e6a691
--- /dev/null
@@ -0,0 +1,43 @@
+#include "cache.h"
+#include "diff.h"
+#include "diffcore.h"
+#include "delta.h"
+#include "count-delta.h"
+
+static int diffcore_count_changes_1(void *src, unsigned long src_size,
+                                   void *dst, unsigned long dst_size,
+                                   unsigned long delta_limit,
+                                   unsigned long *src_copied,
+                                   unsigned long *literal_added)
+{
+       void *delta;
+       unsigned long delta_size;
+
+       delta = diff_delta(src, src_size,
+                          dst, dst_size,
+                          &delta_size, delta_limit);
+       if (!delta)
+               /* If delta_limit is exceeded, we have too much differences */
+               return -1;
+
+       /* Estimate the edit size by interpreting delta. */
+       if (count_delta(delta, delta_size, src_copied, literal_added)) {
+               free(delta);
+               return -1;
+       }
+       free(delta);
+       return 0;
+}
+
+int diffcore_count_changes(void *src, unsigned long src_size,
+                          void *dst, unsigned long dst_size,
+                          unsigned long delta_limit,
+                          unsigned long *src_copied,
+                          unsigned long *literal_added)
+{
+       return diffcore_count_changes_1(src, src_size,
+                                       dst, dst_size,
+                                       delta_limit,
+                                       src_copied,
+                                       literal_added);
+}
index ffd126a..55cf1c3 100644 (file)
@@ -4,8 +4,6 @@
 #include "cache.h"
 #include "diff.h"
 #include "diffcore.h"
-#include "delta.h"
-#include "count-delta.h"
 
 /* Table of rename/copy destinations */
 
@@ -135,7 +133,6 @@ static int estimate_similarity(struct diff_filespec *src,
         * match than anything else; the destination does not even
         * call into this function in that case.
         */
-       void *delta;
        unsigned long delta_size, base_size, src_copied, literal_added;
        unsigned long delta_limit;
        int score;
@@ -165,28 +162,13 @@ static int estimate_similarity(struct diff_filespec *src,
        if (diff_populate_filespec(src, 0) || diff_populate_filespec(dst, 0))
                return 0; /* error but caught downstream */
 
-       delta_limit = base_size * (MAX_SCORE-minimum_score) / MAX_SCORE;
-       delta = diff_delta(src->data, src->size,
-                          dst->data, dst->size,
-                          &delta_size, delta_limit);
-       if (!delta)
-               /* If delta_limit is exceeded, we have too much differences */
-               return 0;
-
-       /* A delta that has a lot of literal additions would have
-        * big delta_size no matter what else it does.
-        */
-       if (base_size * (MAX_SCORE-minimum_score) < delta_size * MAX_SCORE) {
-               free(delta);
-               return 0;
-       }
 
-       /* Estimate the edit size by interpreting delta. */
-       if (count_delta(delta, delta_size, &src_copied, &literal_added)) {
-               free(delta);
+       delta_limit = base_size * (MAX_SCORE-minimum_score) / MAX_SCORE;
+       if (diffcore_count_changes(src->data, src->size,
+                                  dst->data, dst->size,
+                                  delta_limit,
+                                  &src_copied, &literal_added))
                return 0;
-       }
-       free(delta);
 
        /* Extent of damage */
        if (src->size + literal_added < src_copied)
index 12cd816..dba4f17 100644 (file)
@@ -101,4 +101,10 @@ void diff_debug_queue(const char *, struct diff_queue_struct *);
 #define diff_debug_queue(a,b) do {} while(0)
 #endif
 
+extern int diffcore_count_changes(void *src, unsigned long src_size,
+                                 void *dst, unsigned long dst_size,
+                                 unsigned long delta_limit,
+                                 unsigned long *src_copied,
+                                 unsigned long *literal_added);
+
 #endif
diff --git a/entry.c b/entry.c
index 6c47c3a..8fb99bc 100644 (file)
--- a/entry.c
+++ b/entry.c
@@ -123,7 +123,7 @@ int checkout_entry(struct cache_entry *ce, struct checkout *state)
        strcpy(path + len, ce->name);
 
        if (!lstat(path, &st)) {
-               unsigned changed = ce_match_stat(ce, &st);
+               unsigned changed = ce_match_stat(ce, &st, 1);
                if (!changed)
                        return 0;
                if (!state->force) {
index 73e4b1c..16c08f0 100644 (file)
@@ -12,6 +12,7 @@
 char git_default_email[MAX_GITNAME];
 char git_default_name[MAX_GITNAME];
 int trust_executable_bit = 1;
+int assume_unchanged = 0;
 int only_use_symrefs = 0;
 int repository_format_version = 0;
 char git_commit_encoding[MAX_ENCODING_LENGTH] = "utf-8";
index 55af33b..b5e59a9 100644 (file)
@@ -13,7 +13,7 @@ void git_set_exec_path(const char *exec_path)
 
 
 /* Returns the highest-priority, location to look for git programs. */
-const char *git_exec_path()
+const char *git_exec_path(void)
 {
        const char *env;
 
index aa6f42a..535de10 100644 (file)
@@ -8,7 +8,7 @@ static int keep_pack;
 static int quiet;
 static int verbose;
 static const char fetch_pack_usage[] =
-"git-fetch-pack [-q] [-v] [-k] [--exec=upload-pack] [host:]directory <refs>...";
+"git-fetch-pack [-q] [-v] [-k] [--thin] [--exec=upload-pack] [host:]directory <refs>...";
 static const char *exec = "git-upload-pack";
 
 #define COMPLETE       (1U << 0)
@@ -18,7 +18,7 @@ static const char *exec = "git-upload-pack";
 #define POPPED         (1U << 4)
 
 static struct commit_list *rev_list = NULL;
-static int non_common_revs = 0, multi_ack = 0;
+static int non_common_revs = 0, multi_ack = 0, use_thin_pack = 0;
 
 static void rev_list_push(struct commit *commit, int mark)
 {
@@ -82,7 +82,7 @@ static void mark_common(struct commit *commit,
   Get the next rev to send, ignoring the common.
 */
 
-static const unsigned char* get_rev()
+static const unsigned char* get_rev(void)
 {
        struct commit *commit = NULL;
 
@@ -156,8 +156,9 @@ static int find_common(int fd[2], unsigned char *result_sha1,
                        continue;
                }
 
-               packet_write(fd[1], "want %s%s\n", sha1_to_hex(remote),
-                       multi_ack ? " multi_ack" : "");
+               packet_write(fd[1], "want %s%s%s\n", sha1_to_hex(remote),
+                            (multi_ack ? " multi_ack" : ""),
+                            (use_thin_pack ? " thin-pack" : ""));
                fetching++;
        }
        packet_flush(fd[1]);
@@ -421,6 +422,10 @@ int main(int argc, char **argv)
                                keep_pack = 1;
                                continue;
                        }
+                       if (!strcmp("--thin", arg)) {
+                               use_thin_pack = 1;
+                               continue;
+                       }
                        if (!strcmp("-v", arg)) {
                                verbose = 1;
                                continue;
@@ -434,6 +439,8 @@ int main(int argc, char **argv)
        }
        if (!dest)
                usage(fetch_pack_usage);
+       if (keep_pack)
+               use_thin_pack = 0;
        pid = git_connect(fd, dest, exec);
        if (pid < 0)
                return 1;
index 6439d55..4ddd676 100644 (file)
@@ -20,7 +20,7 @@ static int check_strict = 0;
 static int keep_cache_objects = 0; 
 static unsigned char head_sha1[20];
 
-#if NO_D_INO_IN_DIRENT
+#ifdef NO_D_INO_IN_DIRENT
 #define SORT_DIRENT 0
 #define DIRENT_SORT_HINT(de) 0
 #else
index 611f152..d6a4bc7 100755 (executable)
@@ -28,6 +28,17 @@ while : ; do
   shift
 done
 
+# Check misspelled pathspec
+case "$#" in
+0)     ;;
+*)
+       git-ls-files --error-unmatch --others --cached -- "$@" >/dev/null || {
+               echo >&2 "Maybe you misspelled it?"
+               exit 1
+       }
+       ;;
+esac
+
 if test -f "$GIT_DIR/info/exclude"
 then
        git-ls-files -z \
diff --git a/git-annotate.perl b/git-annotate.perl
new file mode 100755 (executable)
index 0000000..cd476c7
--- /dev/null
@@ -0,0 +1,495 @@
+#!/usr/bin/perl
+# Copyright 2006, Ryan Anderson <ryan@michonline.com>
+#
+# GPL v2 (See COPYING)
+#
+# This file is licensed under the GPL v2, or a later version
+# at the discretion of Linus Torvalds.
+
+use warnings;
+use strict;
+use Getopt::Long;
+use POSIX qw(strftime gmtime);
+
+sub usage() {
+       print STDERR 'Usage: ${\basename $0} [-s] [-S revs-file] file [ revision ]
+       -l, --long
+                       Show long rev (Defaults off)
+       -r, --rename
+                       Follow renames (Defaults on).
+       -S, --rev-file revs-file
+                       use revs from revs-file instead of calling git-rev-list
+       -h, --help
+                       This message.
+';
+
+       exit(1);
+}
+
+our ($help, $longrev, $rename, $starting_rev, $rev_file) = (0, 0, 1);
+
+my $rc = GetOptions(   "long|l" => \$longrev,
+                       "help|h" => \$help,
+                       "rename|r" => \$rename,
+                       "rev-file|S=s" => \$rev_file);
+if (!$rc or $help) {
+       usage();
+}
+
+my $filename = shift @ARGV;
+if (@ARGV) {
+       $starting_rev = shift @ARGV;
+}
+
+my @stack = (
+       {
+               'rev' => defined $starting_rev ? $starting_rev : "HEAD",
+               'filename' => $filename,
+       },
+);
+
+our @filelines = ();
+
+if (defined $starting_rev) {
+       @filelines = git_cat_file($starting_rev, $filename);
+} else {
+       open(F,"<",$filename)
+               or die "Failed to open filename: $!";
+
+       while(<F>) {
+               chomp;
+               push @filelines, $_;
+       }
+       close(F);
+
+}
+
+our %revs;
+our @revqueue;
+our $head;
+
+my $revsprocessed = 0;
+while (my $bound = pop @stack) {
+       my @revisions = git_rev_list($bound->{'rev'}, $bound->{'filename'});
+       foreach my $revinst (@revisions) {
+               my ($rev, @parents) = @$revinst;
+               $head ||= $rev;
+
+               if (!defined($rev)) {
+                       $rev = "";
+               }
+               $revs{$rev}{'filename'} = $bound->{'filename'};
+               if (scalar @parents > 0) {
+                       $revs{$rev}{'parents'} = \@parents;
+                       next;
+               }
+
+               if (!$rename) {
+                       next;
+               }
+
+               my $newbound = find_parent_renames($rev, $bound->{'filename'});
+               if ( exists $newbound->{'filename'} && $newbound->{'filename'} ne $bound->{'filename'}) {
+                       push @stack, $newbound;
+                       $revs{$rev}{'parents'} = [$newbound->{'rev'}];
+               }
+       }
+}
+push @revqueue, $head;
+init_claim( defined $starting_rev ? $starting_rev : 'dirty');
+unless (defined $starting_rev) {
+       my $diff = open_pipe("git","diff","-R", "HEAD", "--",$filename)
+               or die "Failed to call git diff to check for dirty state: $!";
+
+       _git_diff_parse($diff, $head, "dirty", (
+                               'author' => gitvar_name("GIT_AUTHOR_IDENT"),
+                               'author_date' => sprintf("%s +0000",time()),
+                               )
+                       );
+       close($diff);
+}
+handle_rev();
+
+
+my $i = 0;
+foreach my $l (@filelines) {
+       my ($output, $rev, $committer, $date);
+       if (ref $l eq 'ARRAY') {
+               ($output, $rev, $committer, $date) = @$l;
+               if (!$longrev && length($rev) > 8) {
+                       $rev = substr($rev,0,8);
+               }
+       } else {
+               $output = $l;
+               ($rev, $committer, $date) = ('unknown', 'unknown', 'unknown');
+       }
+
+       printf("%s\t(%10s\t%10s\t%d)%s\n", $rev, $committer,
+               format_date($date), $i++, $output);
+}
+
+sub init_claim {
+       my ($rev) = @_;
+       for (my $i = 0; $i < @filelines; $i++) {
+               $filelines[$i] = [ $filelines[$i], '', '', '', 1];
+                       # line,
+                       # rev,
+                       # author,
+                       # date,
+                       # 1 <-- belongs to the original file.
+       }
+       $revs{$rev}{'lines'} = \@filelines;
+}
+
+
+sub handle_rev {
+       my $i = 0;
+       my %seen;
+       while (my $rev = shift @revqueue) {
+               next if $seen{$rev}++;
+
+               my %revinfo = git_commit_info($rev);
+
+               foreach my $p (@{$revs{$rev}{'parents'}}) {
+
+                       git_diff_parse($p, $rev, %revinfo);
+                       push @revqueue, $p;
+               }
+
+
+               if (scalar @{$revs{$rev}{parents}} == 0) {
+                       # We must be at the initial rev here, so claim everything that is left.
+                       for (my $i = 0; $i < @{$revs{$rev}{lines}}; $i++) {
+                               if (ref ${$revs{$rev}{lines}}[$i] eq '' || ${$revs{$rev}{lines}}[$i][1] eq '') {
+                                       claim_line($i, $rev, $revs{$rev}{lines}, %revinfo);
+                               }
+                       }
+               }
+       }
+}
+
+
+sub git_rev_list {
+       my ($rev, $file) = @_;
+
+       my $revlist;
+       if ($rev_file) {
+               open($revlist, '<' . $rev_file)
+                   or die "Failed to open $rev_file : $!";
+       } else {
+               $revlist = open_pipe("git-rev-list","--parents","--remove-empty",$rev,"--",$file)
+                       or die "Failed to exec git-rev-list: $!";
+       }
+
+       my @revs;
+       while(my $line = <$revlist>) {
+               chomp $line;
+               my ($rev, @parents) = split /\s+/, $line;
+               push @revs, [ $rev, @parents ];
+       }
+       close($revlist);
+
+       printf("0 revs found for rev %s (%s)\n", $rev, $file) if (@revs == 0);
+       return @revs;
+}
+
+sub find_parent_renames {
+       my ($rev, $file) = @_;
+
+       my $patch = open_pipe("git-diff-tree", "-M50", "-r","--name-status", "-z","$rev")
+               or die "Failed to exec git-diff: $!";
+
+       local $/ = "\0";
+       my %bound;
+       my $junk = <$patch>;
+       while (my $change = <$patch>) {
+               chomp $change;
+               my $filename = <$patch>;
+               chomp $filename;
+
+               if ($change =~ m/^[AMD]$/ ) {
+                       next;
+               } elsif ($change =~ m/^R/ ) {
+                       my $oldfilename = $filename;
+                       $filename = <$patch>;
+                       chomp $filename;
+                       if ( $file eq $filename ) {
+                               my $parent = git_find_parent($rev, $oldfilename);
+                               @bound{'rev','filename'} = ($parent, $oldfilename);
+                               last;
+                       }
+               }
+       }
+       close($patch);
+
+       return \%bound;
+}
+
+
+sub git_find_parent {
+       my ($rev, $filename) = @_;
+
+       my $revparent = open_pipe("git-rev-list","--remove-empty", "--parents","--max-count=1","$rev","--",$filename)
+               or die "Failed to open git-rev-list to find a single parent: $!";
+
+       my $parentline = <$revparent>;
+       chomp $parentline;
+       my ($revfound,$parent) = split m/\s+/, $parentline;
+
+       close($revparent);
+
+       return $parent;
+}
+
+
+# Get a diff between the current revision and a parent.
+# Record the commit information that results.
+sub git_diff_parse {
+       my ($parent, $rev, %revinfo) = @_;
+
+       my $diff = open_pipe("git-diff-tree","-M","-p",$rev,$parent,"--",
+                       $revs{$rev}{'filename'}, $revs{$parent}{'filename'})
+               or die "Failed to call git-diff for annotation: $!";
+
+       _git_diff_parse($diff, $parent, $rev, %revinfo);
+
+       close($diff);
+}
+
+sub _git_diff_parse {
+       my ($diff, $parent, $rev, %revinfo) = @_;
+
+       my ($ri, $pi) = (0,0);
+       my $slines = $revs{$rev}{'lines'};
+       my @plines;
+
+       my $gotheader = 0;
+       my ($remstart);
+       my ($hunk_start, $hunk_index);
+       while(<$diff>) {
+               chomp;
+               if (m/^@@ -(\d+),(\d+) \+(\d+),(\d+)/) {
+                       $remstart = $1;
+                       # Adjust for 0-based arrays
+                       $remstart--;
+                       # Reinit hunk tracking.
+                       $hunk_start = $remstart;
+                       $hunk_index = 0;
+                       $gotheader = 1;
+
+                       for (my $i = $ri; $i < $remstart; $i++) {
+                               $plines[$pi++] = $slines->[$i];
+                               $ri++;
+                       }
+                       next;
+               } elsif (!$gotheader) {
+                       next;
+               }
+
+               if (m/^\+(.*)$/) {
+                       my $line = $1;
+                       $plines[$pi++] = [ $line, '', '', '', 0 ];
+                       next;
+
+               } elsif (m/^-(.*)$/) {
+                       my $line = $1;
+                       if (get_line($slines, $ri) eq $line) {
+                               # Found a match, claim
+                               claim_line($ri, $rev, $slines, %revinfo);
+                       } else {
+                               die sprintf("Sync error: %d/%d\n|%s\n|%s\n%s => %s\n",
+                                               $ri, $hunk_start + $hunk_index,
+                                               $line,
+                                               get_line($slines, $ri),
+                                               $rev, $parent);
+                       }
+                       $ri++;
+
+               } elsif (m/^\\/) {
+                       ;
+                       # Skip \No newline at end of file.
+                       # But this can be internationalized, so only look
+                       # for an initial \
+
+               } else {
+                       if (substr($_,1) ne get_line($slines,$ri) ) {
+                               die sprintf("Line %d (%d) does not match:\n|%s\n|%s\n%s => %s\n",
+                                               $hunk_start + $hunk_index, $ri,
+                                               substr($_,1),
+                                               get_line($slines,$ri),
+                                               $rev, $parent);
+                       }
+                       $plines[$pi++] = $slines->[$ri++];
+               }
+               $hunk_index++;
+       }
+       for (my $i = $ri; $i < @{$slines} ; $i++) {
+               push @plines, $slines->[$ri++];
+       }
+
+       $revs{$parent}{lines} = \@plines;
+       return;
+}
+
+sub get_line {
+       my ($lines, $index) = @_;
+
+       return ref $lines->[$index] ne '' ? $lines->[$index][0] : $lines->[$index];
+}
+
+sub git_cat_file {
+       my ($rev, $filename) = @_;
+       return () unless defined $rev && defined $filename;
+
+       my $blob = git_ls_tree($rev, $filename);
+
+       my $catfile = open_pipe("git","cat-file", "blob", $blob)
+               or die "Failed to git-cat-file blob $blob (rev $rev, file $filename): " . $!;
+
+       my @lines;
+       while(<$catfile>) {
+               chomp;
+               push @lines, $_;
+       }
+       close($catfile);
+
+       return @lines;
+}
+
+sub git_ls_tree {
+       my ($rev, $filename) = @_;
+
+       my $lstree = open_pipe("git","ls-tree",$rev,$filename)
+               or die "Failed to call git ls-tree: $!";
+
+       my ($mode, $type, $blob, $tfilename);
+       while(<$lstree>) {
+               ($mode, $type, $blob, $tfilename) = split(/\s+/, $_, 4);
+               last if ($tfilename eq $filename);
+       }
+       close($lstree);
+
+       return $blob if $filename eq $filename;
+       die "git-ls-tree failed to find blob for $filename";
+
+}
+
+
+
+sub claim_line {
+       my ($floffset, $rev, $lines, %revinfo) = @_;
+       my $oline = get_line($lines, $floffset);
+       @{$lines->[$floffset]} = ( $oline, $rev,
+               $revinfo{'author'}, $revinfo{'author_date'} );
+       #printf("Claiming line %d with rev %s: '%s'\n",
+       #               $floffset, $rev, $oline) if 1;
+}
+
+sub git_commit_info {
+       my ($rev) = @_;
+       my $commit = open_pipe("git-cat-file", "commit", $rev)
+               or die "Failed to call git-cat-file: $!";
+
+       my %info;
+       while(<$commit>) {
+               chomp;
+               last if (length $_ == 0);
+
+               if (m/^author (.*) <(.*)> (.*)$/) {
+                       $info{'author'} = $1;
+                       $info{'author_email'} = $2;
+                       $info{'author_date'} = $3;
+               } elsif (m/^committer (.*) <(.*)> (.*)$/) {
+                       $info{'committer'} = $1;
+                       $info{'committer_email'} = $2;
+                       $info{'committer_date'} = $3;
+               }
+       }
+       close($commit);
+
+       return %info;
+}
+
+sub format_date {
+       my ($timestamp, $timezone) = split(' ', $_[0]);
+
+       return strftime("%Y-%m-%d %H:%M:%S " . $timezone, gmtime($timestamp));
+}
+
+# Copied from git-send-email.perl - We need a Git.pm module..
+sub gitvar {
+    my ($var) = @_;
+    my $fh;
+    my $pid = open($fh, '-|');
+    die "$!" unless defined $pid;
+    if (!$pid) {
+       exec('git-var', $var) or die "$!";
+    }
+    my ($val) = <$fh>;
+    close $fh or die "$!";
+    chomp($val);
+    return $val;
+}
+
+sub gitvar_name {
+    my ($name) = @_;
+    my $val = gitvar($name);
+    my @field = split(/\s+/, $val);
+    return join(' ', @field[0...(@field-4)]);
+}
+
+sub open_pipe {
+       if ($^O eq '##INSERT_ACTIVESTATE_STRING_HERE##') {
+               return open_pipe_activestate(@_);
+       } else {
+               return open_pipe_normal(@_);
+       }
+}
+
+sub open_pipe_activestate {
+       tie *fh, "Git::ActiveStatePipe", @_;
+       return *fh;
+}
+
+sub open_pipe_normal {
+       my (@execlist) = @_;
+
+       my $pid = open my $kid, "-|";
+       defined $pid or die "Cannot fork: $!";
+
+       unless ($pid) {
+               exec @execlist;
+               die "Cannot exec @execlist: $!";
+       }
+
+       return $kid;
+}
+
+package Git::ActiveStatePipe;
+use strict;
+
+sub TIEHANDLE {
+       my ($class, @params) = @_;
+       my $cmdline = join " ", @params;
+       my  @data = qx{$cmdline};
+       bless { i => 0, data => \@data }, $class;
+}
+
+sub READLINE {
+       my $self = shift;
+       if ($self->{i} >= scalar @{$self->{data}}) {
+               return undef;
+       }
+       return $self->{'data'}->[ $self->{i}++ ];
+}
+
+sub CLOSE {
+       my $self = shift;
+       delete $self->{data};
+       delete $self->{i};
+}
+
+sub EOF {
+       my $self = shift;
+       return ($self->{i} >= scalar @{$self->{data}});
+}
index 0750253..03df143 100755 (executable)
@@ -49,9 +49,16 @@ bisect_start() {
        die "Bad HEAD - I need a symbolic ref"
        case "$head" in
        refs/heads/bisect*)
-               git checkout master || exit
+               if [ -s "$GIT_DIR/head-name" ]; then
+                   branch=`cat "$GIT_DIR/head-name"`
+               else
+                   branch=master
+               fi
+               git checkout $branch || exit
                ;;
        refs/heads/*)
+               [ -s "$GIT_DIR/head-name" ] && die "won't bisect on seeked tree"
+               echo "$head" | sed 's#^refs/heads/##' >"$GIT_DIR/head-name"
                ;;
        *)
                die "Bad HEAD - strange symbolic ref"
@@ -159,7 +166,11 @@ bisect_visualize() {
 
 bisect_reset() {
        case "$#" in
-       0) branch=master ;;
+       0) if [ -s "$GIT_DIR/head-name" ]; then
+              branch=`cat "$GIT_DIR/head-name"`
+          else
+              branch=master
+          fi ;;
        1) test -f "$GIT_DIR/refs/heads/$1" || {
               echo >&2 "$1 does not seem to be a valid branch"
               exit 1
@@ -170,7 +181,7 @@ bisect_reset() {
        esac
        git checkout "$branch" &&
        rm -fr "$GIT_DIR/refs/bisect"
-       rm -f "$GIT_DIR/refs/heads/bisect"
+       rm -f "$GIT_DIR/refs/heads/bisect" "$GIT_DIR/head-name"
        rm -f "$GIT_DIR/BISECT_LOG"
        rm -f "$GIT_DIR/BISECT_NAMES"
 }
index 89c15d7..4ed861d 100755 (executable)
@@ -154,7 +154,7 @@ yes,yes)
            fi &&
            rm -f "$GIT_DIR/objects/sample" &&
            cd "$repo" &&
-           find objects -depth -print | cpio -puamd$l "$GIT_DIR/" || exit 1
+           find objects -depth -print | cpio -pumd$l "$GIT_DIR/" || exit 1
            ;;
        yes)
            mkdir -p "$GIT_DIR/objects/info"
index 59551d9..f7ee1aa 100755 (executable)
@@ -3,7 +3,7 @@
 # Copyright (c) 2005 Linus Torvalds
 # Copyright (c) 2006 Junio C Hamano
 
-USAGE='[-a] [-i] [-s] [-v] [--no-verify] [-m <message> | -F <logfile> | (-C|-c) <commit>] [-e] [--author <author>] [<path>...]'
+USAGE='[-a] [-s] [-v] [--no-verify] [-m <message> | -F <logfile> | (-C|-c) <commit>] [-e] [--author <author>] [[-i | -o] <path>...]'
 SUBDIRECTORY_OK=Yes
 . git-sh-setup
 
@@ -180,6 +180,7 @@ verify=t
 verbose=
 signoff=
 force_author=
+only_include_assumed=
 while case "$#" in 0) break;; esac
 do
   case "$1" in
@@ -340,15 +341,8 @@ case "$#,$also$only" in
 0,)
   ;;
 *,)
-  echo >&2 "assuming --include paths..."
-  also=t
-  # Later when switch the defaults, we will replace them with these:
-  # echo >&2 "assuming --only paths..."
-  # also=
-
-  # If we are going to launch an editor, the message won't be
-  # shown without this...
-  test -z "$log_given$status_only" && sleep 1
+  only_include_assumed="# Explicit paths specified without -i nor -o; assuming --only paths..."
+  also=
   ;;
 esac
 unset only
@@ -383,6 +377,8 @@ t,)
        ;;
 ,t)
        save_index &&
+       git-ls-files --error-unmatch -- "$@" >/dev/null || exit
+
        git-diff-files --name-only -z -- "$@"  |
        (
                cd "$TOP"
@@ -411,7 +407,7 @@ t,)
                refuse_partial "Different in index and the last commit:
 $dirty_in_index"
            fi
-           commit_only=`git-ls-files -- "$@"`
+           commit_only=`git-ls-files --error-unmatch -- "$@"` || exit
 
            # Build the temporary index and update the real index
            # the same way.
@@ -572,7 +568,10 @@ else
        PARENTS=""
 fi
 
-run_status >>"$GIT_DIR"/COMMIT_EDITMSG
+{
+       test -z "$only_include_assumed" || echo "$only_include_assumed"
+       run_status
+} >>"$GIT_DIR"/COMMIT_EDITMSG
 if [ "$?" != "0" -a ! -f "$GIT_DIR/MERGE_HEAD" ]
 then
        rm -f "$GIT_DIR/COMMIT_EDITMSG"
index 24f9834..b46469a 100755 (executable)
@@ -846,8 +846,12 @@ while(<CVS>) {
                        print "Drop $fn\n" if $opt_v;
                } else {
                        print "".($init ? "New" : "Update")." $fn: $size bytes\n" if $opt_v;
-                       open my $F, '-|', "git-hash-object -w $tmpname"
+                       my $pid = open(my $F, '-|');
+                       die $! unless defined $pid;
+                       if (!$pid) {
+                           exec("git-hash-object", "-w", $tmpname)
                                or die "Cannot create object: $!\n";
+                       }
                        my $sha = <$F>;
                        chomp $sha;
                        close $F;
diff --git a/git-cvsserver.perl b/git-cvsserver.perl
new file mode 100755 (executable)
index 0000000..3c588c9
--- /dev/null
@@ -0,0 +1,2487 @@
+#!/usr/bin/perl
+
+####
+#### This application is a CVS emulation layer for git.
+#### It is intended for clients to connect over SSH.
+#### See the documentation for more details.
+####
+#### Copyright The Open University UK - 2006.
+####
+#### Authors: Martyn Smith    <martyn@catalyst.net.nz>
+####          Martin Langhoff <martin@catalyst.net.nz>
+####
+####
+#### Released under the GNU Public License, version 2.
+####
+####
+
+use strict;
+use warnings;
+
+use Fcntl;
+use File::Temp qw/tempdir tempfile/;
+use File::Basename;
+
+my $log = GITCVS::log->new();
+my $cfg;
+
+my $DATE_LIST = {
+    Jan => "01",
+    Feb => "02",
+    Mar => "03",
+    Apr => "04",
+    May => "05",
+    Jun => "06",
+    Jul => "07",
+    Aug => "08",
+    Sep => "09",
+    Oct => "10",
+    Nov => "11",
+    Dec => "12",
+};
+
+# Enable autoflush for STDOUT (otherwise the whole thing falls apart)
+$| = 1;
+
+#### Definition and mappings of functions ####
+
+my $methods = {
+    'Root'            => \&req_Root,
+    'Valid-responses' => \&req_Validresponses,
+    'valid-requests'  => \&req_validrequests,
+    'Directory'       => \&req_Directory,
+    'Entry'           => \&req_Entry,
+    'Modified'        => \&req_Modified,
+    'Unchanged'       => \&req_Unchanged,
+    'Questionable'    => \&req_Questionable,
+    'Argument'        => \&req_Argument,
+    'Argumentx'       => \&req_Argument,
+    'expand-modules'  => \&req_expandmodules,
+    'add'             => \&req_add,
+    'remove'          => \&req_remove,
+    'co'              => \&req_co,
+    'update'          => \&req_update,
+    'ci'              => \&req_ci,
+    'diff'            => \&req_diff,
+    'log'             => \&req_log,
+    'rlog'            => \&req_log,
+    'tag'             => \&req_CATCHALL,
+    'status'          => \&req_status,
+    'admin'           => \&req_CATCHALL,
+    'history'         => \&req_CATCHALL,
+    'watchers'        => \&req_CATCHALL,
+    'editors'         => \&req_CATCHALL,
+    'annotate'        => \&req_annotate,
+    'Global_option'   => \&req_Globaloption,
+    #'annotate'        => \&req_CATCHALL,
+};
+
+##############################################
+
+
+# $state holds all the bits of information the clients sends us that could
+# potentially be useful when it comes to actually _doing_ something.
+my $state = {};
+$log->info("--------------- STARTING -----------------");
+
+my $TEMP_DIR = tempdir( CLEANUP => 1 );
+$log->debug("Temporary directory is '$TEMP_DIR'");
+
+# Keep going until the client closes the connection
+while (<STDIN>)
+{
+    chomp;
+
+    # Check to see if we've seen this method, and call appropiate function.
+    if ( /^([\w-]+)(?:\s+(.*))?$/ and defined($methods->{$1}) )
+    {
+        # use the $methods hash to call the appropriate sub for this command
+        #$log->info("Method : $1");
+        &{$methods->{$1}}($1,$2);
+    } else {
+        # log fatal because we don't understand this function. If this happens
+        # we're fairly screwed because we don't know if the client is expecting
+        # a response. If it is, the client will hang, we'll hang, and the whole
+        # thing will be custard.
+        $log->fatal("Don't understand command $_\n");
+        die("Unknown command $_");
+    }
+}
+
+$log->debug("Processing time : user=" . (times)[0] . " system=" . (times)[1]);
+$log->info("--------------- FINISH -----------------");
+
+# Magic catchall method.
+#    This is the method that will handle all commands we haven't yet
+#    implemented. It simply sends a warning to the log file indicating a
+#    command that hasn't been implemented has been invoked.
+sub req_CATCHALL
+{
+    my ( $cmd, $data ) = @_;
+    $log->warn("Unhandled command : req_$cmd : $data");
+}
+
+
+# Root pathname \n
+#     Response expected: no. Tell the server which CVSROOT to use. Note that
+#     pathname is a local directory and not a fully qualified CVSROOT variable.
+#     pathname must already exist; if creating a new root, use the init
+#     request, not Root. pathname does not include the hostname of the server,
+#     how to access the server, etc.; by the time the CVS protocol is in use,
+#     connection, authentication, etc., are already taken care of. The Root
+#     request must be sent only once, and it must be sent before any requests
+#     other than Valid-responses, valid-requests, UseUnchanged, Set or init.
+sub req_Root
+{
+    my ( $cmd, $data ) = @_;
+    $log->debug("req_Root : $data");
+
+    $state->{CVSROOT} = $data;
+
+    $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
+
+    foreach my $line ( `git-var -l` )
+    {
+        next unless ( $line =~ /^(.*?)\.(.*?)=(.*)$/ );
+        $cfg->{$1}{$2} = $3;
+    }
+
+    unless ( defined ( $cfg->{gitcvs}{enabled} ) and $cfg->{gitcvs}{enabled} =~ /^\s*(1|true|yes)\s*$/i )
+    {
+        print "E GITCVS emulation needs to be enabled on this repo\n";
+        print "E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n";
+        print "E \n";
+        print "error 1 GITCVS emulation disabled\n";
+    }
+
+    if ( defined ( $cfg->{gitcvs}{logfile} ) )
+    {
+        $log->setfile($cfg->{gitcvs}{logfile});
+    } else {
+        $log->nofile();
+    }
+}
+
+# Global_option option \n
+#     Response expected: no. Transmit one of the global options `-q', `-Q',
+#     `-l', `-t', `-r', or `-n'. option must be one of those strings, no
+#     variations (such as combining of options) are allowed. For graceful
+#     handling of valid-requests, it is probably better to make new global
+#     options separate requests, rather than trying to add them to this
+#     request.
+sub req_Globaloption
+{
+    my ( $cmd, $data ) = @_;
+    $log->debug("req_Globaloption : $data");
+
+    # TODO : is this data useful ???
+}
+
+# Valid-responses request-list \n
+#     Response expected: no. Tell the server what responses the client will
+#     accept. request-list is a space separated list of tokens.
+sub req_Validresponses
+{
+    my ( $cmd, $data ) = @_;
+    $log->debug("req_Validrepsonses : $data");
+
+    # TODO : re-enable this, currently it's not particularly useful
+    #$state->{validresponses} = [ split /\s+/, $data ];
+}
+
+# valid-requests \n
+#     Response expected: yes. Ask the server to send back a Valid-requests
+#     response.
+sub req_validrequests
+{
+    my ( $cmd, $data ) = @_;
+
+    $log->debug("req_validrequests");
+
+    $log->debug("SEND : Valid-requests " . join(" ",keys %$methods));
+    $log->debug("SEND : ok");
+
+    print "Valid-requests " . join(" ",keys %$methods) . "\n";
+    print "ok\n";
+}
+
+# Directory local-directory \n
+#     Additional data: repository \n. Response expected: no. Tell the server
+#     what directory to use. The repository should be a directory name from a
+#     previous server response. Note that this both gives a default for Entry
+#     and Modified and also for ci and the other commands; normal usage is to
+#     send Directory for each directory in which there will be an Entry or
+#     Modified, and then a final Directory for the original directory, then the
+#     command. The local-directory is relative to the top level at which the
+#     command is occurring (i.e. the last Directory which is sent before the
+#     command); to indicate that top level, `.' should be sent for
+#     local-directory.
+sub req_Directory
+{
+    my ( $cmd, $data ) = @_;
+
+    my $repository = <STDIN>;
+    chomp $repository;
+
+
+    $state->{localdir} = $data;
+    $state->{repository} = $repository;
+    $state->{directory} = $repository;
+    $state->{directory} =~ s/^$state->{CVSROOT}\///;
+    $state->{module} = $1 if ($state->{directory} =~ s/^(.*?)(\/|$)//);
+    $state->{directory} .= "/" if ( $state->{directory} =~ /\S/ );
+
+    $log->debug("req_Directory : localdir=$data repository=$repository directory=$state->{directory} module=$state->{module}");
+}
+
+# Entry entry-line \n
+#     Response expected: no. Tell the server what version of a file is on the
+#     local machine. The name in entry-line is a name relative to the directory
+#     most recently specified with Directory. If the user is operating on only
+#     some files in a directory, Entry requests for only those files need be
+#     included. If an Entry request is sent without Modified, Is-modified, or
+#     Unchanged, it means the file is lost (does not exist in the working
+#     directory). If both Entry and one of Modified, Is-modified, or Unchanged
+#     are sent for the same file, Entry must be sent first. For a given file,
+#     one can send Modified, Is-modified, or Unchanged, but not more than one
+#     of these three.
+sub req_Entry
+{
+    my ( $cmd, $data ) = @_;
+
+    $log->debug("req_Entry : $data");
+
+    my @data = split(/\//, $data);
+
+    $state->{entries}{$state->{directory}.$data[1]} = {
+        revision    => $data[2],
+        conflict    => $data[3],
+        options     => $data[4],
+        tag_or_date => $data[5],
+    };
+}
+
+# add \n
+#     Response expected: yes. Add a file or directory. This uses any previous
+#     Argument, Directory, Entry, or Modified requests, if they have been sent.
+#     The last Directory sent specifies the working directory at the time of
+#     the operation. To add a directory, send the directory to be added using
+#     Directory and Argument requests.
+sub req_add
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit("add");
+
+    my $addcount = 0;
+
+    foreach my $filename ( @{$state->{args}} )
+    {
+        $filename = filecleanup($filename);
+
+        unless ( defined ( $state->{entries}{$filename}{modified_filename} ) )
+        {
+            print "E cvs add: nothing known about `$filename'\n";
+            next;
+        }
+        # TODO : check we're not squashing an already existing file
+        if ( defined ( $state->{entries}{$filename}{revision} ) )
+        {
+            print "E cvs add: `$filename' has already been entered\n";
+            next;
+        }
+
+
+        my ( $filepart, $dirpart ) = filenamesplit($filename);
+
+        print "E cvs add: scheduling file `$filename' for addition\n";
+
+        print "Checked-in $dirpart\n";
+        print "$filename\n";
+        print "/$filepart/0///\n";
+
+        $addcount++;
+    }
+
+    if ( $addcount == 1 )
+    {
+        print "E cvs add: use `cvs commit' to add this file permanently\n";
+    }
+    elsif ( $addcount > 1 )
+    {
+        print "E cvs add: use `cvs commit' to add these files permanently\n";
+    }
+
+    print "ok\n";
+}
+
+# remove \n
+#     Response expected: yes. Remove a file. This uses any previous Argument,
+#     Directory, Entry, or Modified requests, if they have been sent. The last
+#     Directory sent specifies the working directory at the time of the
+#     operation. Note that this request does not actually do anything to the
+#     repository; the only effect of a successful remove request is to supply
+#     the client with a new entries line containing `-' to indicate a removed
+#     file. In fact, the client probably could perform this operation without
+#     contacting the server, although using remove may cause the server to
+#     perform a few more checks. The client sends a subsequent ci request to
+#     actually record the removal in the repository.
+sub req_remove
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit("remove");
+
+    # Grab a handle to the SQLite db and do any necessary updates
+    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+    $updater->update();
+
+    #$log->debug("add state : " . Dumper($state));
+
+    my $rmcount = 0;
+
+    foreach my $filename ( @{$state->{args}} )
+    {
+        $filename = filecleanup($filename);
+
+        if ( defined ( $state->{entries}{$filename}{unchanged} ) or defined ( $state->{entries}{$filename}{modified_filename} ) )
+        {
+            print "E cvs remove: file `$filename' still in working directory\n";
+            next;
+        }
+
+        my $meta = $updater->getmeta($filename);
+        my $wrev = revparse($filename);
+
+        unless ( defined ( $wrev ) )
+        {
+            print "E cvs remove: nothing known about `$filename'\n";
+            next;
+        }
+
+        if ( defined($wrev) and $wrev < 0 )
+        {
+            print "E cvs remove: file `$filename' already scheduled for removal\n";
+            next;
+        }
+
+        unless ( $wrev == $meta->{revision} )
+        {
+            # TODO : not sure if the format of this message is quite correct.
+            print "E cvs remove: Up to date check failed for `$filename'\n";
+            next;
+        }
+
+
+        my ( $filepart, $dirpart ) = filenamesplit($filename);
+
+        print "E cvs remove: scheduling `$filename' for removal\n";
+
+        print "Checked-in $dirpart\n";
+        print "$filename\n";
+        print "/$filepart/-1.$wrev///\n";
+
+        $rmcount++;
+    }
+
+    if ( $rmcount == 1 )
+    {
+        print "E cvs remove: use `cvs commit' to remove this file permanently\n";
+    }
+    elsif ( $rmcount > 1 )
+    {
+        print "E cvs remove: use `cvs commit' to remove these files permanently\n";
+    }
+
+    print "ok\n";
+}
+
+# Modified filename \n
+#     Response expected: no. Additional data: mode, \n, file transmission. Send
+#     the server a copy of one locally modified file. filename is a file within
+#     the most recent directory sent with Directory; it must not contain `/'.
+#     If the user is operating on only some files in a directory, only those
+#     files need to be included. This can also be sent without Entry, if there
+#     is no entry for the file.
+sub req_Modified
+{
+    my ( $cmd, $data ) = @_;
+
+    my $mode = <STDIN>;
+    chomp $mode;
+    my $size = <STDIN>;
+    chomp $size;
+
+    # Grab config information
+    my $blocksize = 8192;
+    my $bytesleft = $size;
+    my $tmp;
+
+    # Get a filehandle/name to write it to
+    my ( $fh, $filename ) = tempfile( DIR => $TEMP_DIR );
+
+    # Loop over file data writing out to temporary file.
+    while ( $bytesleft )
+    {
+        $blocksize = $bytesleft if ( $bytesleft < $blocksize );
+        read STDIN, $tmp, $blocksize;
+        print $fh $tmp;
+        $bytesleft -= $blocksize;
+    }
+
+    close $fh;
+
+    # Ensure we have something sensible for the file mode
+    if ( $mode =~ /u=(\w+)/ )
+    {
+        $mode = $1;
+    } else {
+        $mode = "rw";
+    }
+
+    # Save the file data in $state
+    $state->{entries}{$state->{directory}.$data}{modified_filename} = $filename;
+    $state->{entries}{$state->{directory}.$data}{modified_mode} = $mode;
+    $state->{entries}{$state->{directory}.$data}{modified_hash} = `git-hash-object $filename`;
+    $state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s;
+
+    #$log->debug("req_Modified : file=$data mode=$mode size=$size");
+}
+
+# Unchanged filename \n
+#     Response expected: no. Tell the server that filename has not been
+#     modified in the checked out directory. The filename is a file within the
+#     most recent directory sent with Directory; it must not contain `/'.
+sub req_Unchanged
+{
+    my ( $cmd, $data ) = @_;
+
+    $state->{entries}{$state->{directory}.$data}{unchanged} = 1;
+
+    #$log->debug("req_Unchanged : $data");
+}
+
+# Questionable filename \n
+#     Response expected: no. Additional data: no.
+#     Tell the server to check whether filename should be ignored,
+#     and if not, next time the server sends responses, send (in
+#     a M response) `?' followed by the directory and filename.
+#     filename must not contain `/'; it needs to be a file in the
+#     directory named by the most recent Directory request.
+sub req_Questionable
+{
+    my ( $cmd, $data ) = @_;
+
+    $state->{entries}{$state->{directory}.$data}{questionable} = 1;
+
+    #$log->debug("req_Questionable : $data");
+}
+
+# Argument text \n
+#     Response expected: no. Save argument for use in a subsequent command.
+#     Arguments accumulate until an argument-using command is given, at which
+#     point they are forgotten.
+# Argumentx text \n
+#     Response expected: no. Append \n followed by text to the current argument
+#     being saved.
+sub req_Argument
+{
+    my ( $cmd, $data ) = @_;
+
+    # TODO :  Not quite sure how Argument and Argumentx differ, but I assume
+    # it's for multi-line arguments ... somehow ...
+
+    $log->debug("$cmd : $data");
+
+    push @{$state->{arguments}}, $data;
+}
+
+# expand-modules \n
+#     Response expected: yes. Expand the modules which are specified in the
+#     arguments. Returns the data in Module-expansion responses. Note that the
+#     server can assume that this is checkout or export, not rtag or rdiff; the
+#     latter do not access the working directory and thus have no need to
+#     expand modules on the client side. Expand may not be the best word for
+#     what this request does. It does not necessarily tell you all the files
+#     contained in a module, for example. Basically it is a way of telling you
+#     which working directories the server needs to know about in order to
+#     handle a checkout of the specified modules. For example, suppose that the
+#     server has a module defined by
+#   aliasmodule -a 1dir
+#     That is, one can check out aliasmodule and it will take 1dir in the
+#     repository and check it out to 1dir in the working directory. Now suppose
+#     the client already has this module checked out and is planning on using
+#     the co request to update it. Without using expand-modules, the client
+#     would have two bad choices: it could either send information about all
+#     working directories under the current directory, which could be
+#     unnecessarily slow, or it could be ignorant of the fact that aliasmodule
+#     stands for 1dir, and neglect to send information for 1dir, which would
+#     lead to incorrect operation. With expand-modules, the client would first
+#     ask for the module to be expanded:
+sub req_expandmodules
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit();
+
+    $log->debug("req_expandmodules : " . ( defined($data) ? $data : "[NULL]" ) );
+
+    unless ( ref $state->{arguments} eq "ARRAY" )
+    {
+        print "ok\n";
+        return;
+    }
+
+    foreach my $module ( @{$state->{arguments}} )
+    {
+        $log->debug("SEND : Module-expansion $module");
+        print "Module-expansion $module\n";
+    }
+
+    print "ok\n";
+    statecleanup();
+}
+
+# co \n
+#     Response expected: yes. Get files from the repository. This uses any
+#     previous Argument, Directory, Entry, or Modified requests, if they have
+#     been sent. Arguments to this command are module names; the client cannot
+#     know what directories they correspond to except by (1) just sending the
+#     co request, and then seeing what directory names the server sends back in
+#     its responses, and (2) the expand-modules request.
+sub req_co
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit("co");
+
+    my $module = $state->{args}[0];
+    my $checkout_path = $module;
+
+    # use the user specified directory if we're given it
+    $checkout_path = $state->{opt}{d} if ( exists ( $state->{opt}{d} ) );
+
+    $log->debug("req_co : " . ( defined($data) ? $data : "[NULL]" ) );
+
+    $log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'");
+
+    $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
+
+    # Grab a handle to the SQLite db and do any necessary updates
+    my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
+    $updater->update();
+
+    # instruct the client that we're checking out to $checkout_path
+    print "E cvs server: updating $checkout_path\n";
+
+    foreach my $git ( @{$updater->gethead} )
+    {
+        # Don't want to check out deleted files
+        next if ( $git->{filehash} eq "deleted" );
+
+        ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
+
+        # modification time of this file
+        print "Mod-time $git->{modified}\n";
+
+        # print some information to the client
+        print "MT +updated\n";
+        print "MT text U \n";
+        if ( defined ( $git->{dir} ) and $git->{dir} ne "./" )
+        {
+            print "MT fname $checkout_path/$git->{dir}$git->{name}\n";
+        } else {
+            print "MT fname $checkout_path/$git->{name}\n";
+        }
+        print "MT newline\n";
+        print "MT -updated\n";
+
+        # instruct client we're sending a file to put in this path
+        print "Created $checkout_path/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "\n";
+
+        print $state->{CVSROOT} . "/$module/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "$git->{name}\n";
+
+        # this is an "entries" line
+        print "/$git->{name}/1.$git->{revision}///\n";
+        # permissions
+        print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
+
+        # transmit file
+        transmitfile($git->{filehash});
+    }
+
+    print "ok\n";
+
+    statecleanup();
+}
+
+# update \n
+#     Response expected: yes. Actually do a cvs update command. This uses any
+#     previous Argument, Directory, Entry, or Modified requests, if they have
+#     been sent. The last Directory sent specifies the working directory at the
+#     time of the operation. The -I option is not used--files which the client
+#     can decide whether to ignore are not mentioned and the client sends the
+#     Questionable request for others.
+sub req_update
+{
+    my ( $cmd, $data ) = @_;
+
+    $log->debug("req_update : " . ( defined($data) ? $data : "[NULL]" ));
+
+    argsplit("update");
+
+    #
+    # It may just be a client exploring the available heads/modukles
+    # in that case, list them as top level directories and leave it
+    # at that. Eclipse uses this technique to offer you a list of
+    # projects (heads in this case) to checkout.
+    #
+    if ($state->{module} eq '') {
+        print "E cvs update: Updating .\n";
+       opendir HEADS, $state->{CVSROOT} . '/refs/heads';
+       while (my $head = readdir(HEADS)) {
+           if (-f $state->{CVSROOT} . '/refs/heads/' . $head) {
+               print "E cvs update: New directory `$head'\n";
+           }
+       }
+       closedir HEADS;
+       print "ok\n";
+       return 1;
+    }
+
+
+    # Grab a handle to the SQLite db and do any necessary updates
+    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+
+    $updater->update();
+
+    # if no files were specified, we need to work out what files we should be providing status on ...
+    argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
+
+    #$log->debug("update state : " . Dumper($state));
+
+    # foreach file specified on the commandline ...
+    foreach my $filename ( @{$state->{args}} )
+    {
+        $filename = filecleanup($filename);
+
+        # if we have a -C we should pretend we never saw modified stuff
+        if ( exists ( $state->{opt}{C} ) )
+        {
+            delete $state->{entries}{$filename}{modified_hash};
+            delete $state->{entries}{$filename}{modified_filename};
+            $state->{entries}{$filename}{unchanged} = 1;
+        }
+
+        my $meta;
+        if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^1\.(\d+)/ )
+        {
+            $meta = $updater->getmeta($filename, $1);
+        } else {
+            $meta = $updater->getmeta($filename);
+        }
+
+        next unless ( $meta->{revision} );
+
+        my $oldmeta = $meta;
+
+        my $wrev = revparse($filename);
+
+        # If the working copy is an old revision, lets get that version too for comparison.
+        if ( defined($wrev) and $wrev != $meta->{revision} )
+        {
+            $oldmeta = $updater->getmeta($filename, $wrev);
+        }
+
+        #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
+
+        # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified _and_ the user hasn't specified -C
+        next if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{unchanged} and not exists ( $state->{opt}{C} ) );
+
+        if ( $meta->{filehash} eq "deleted" )
+        {
+            my ( $filepart, $dirpart ) = filenamesplit($filename);
+
+            $log->info("Removing '$filename' from working copy (no longer in the repo)");
+
+            print "E cvs update: `$filename' is no longer in the repository\n";
+            print "Removed $dirpart\n";
+            print "$filepart\n";
+        }
+        elsif ( not defined ( $state->{entries}{$filename}{modified_hash} ) or $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} )
+        {
+            $log->info("Updating '$filename'");
+            # normal update, just send the new revision (either U=Update, or A=Add, or R=Remove)
+            print "MT +updated\n";
+            print "MT text U\n";
+            print "MT fname $filename\n";
+            print "MT newline\n";
+            print "MT -updated\n";
+
+            my ( $filepart, $dirpart ) = filenamesplit($filename);
+            $dirpart =~ s/^$state->{directory}//;
+
+            if ( defined ( $wrev ) )
+            {
+                # instruct client we're sending a file to put in this path as a replacement
+                print "Update-existing $dirpart\n";
+                $log->debug("Updating existing file 'Update-existing $dirpart'");
+            } else {
+                # instruct client we're sending a file to put in this path as a new file
+                print "Created $dirpart\n";
+                $log->debug("Creating new file 'Created $dirpart'");
+            }
+            print $state->{CVSROOT} . "/$state->{module}/$filename\n";
+
+            # this is an "entries" line
+            $log->debug("/$filepart/1.$meta->{revision}///");
+            print "/$filepart/1.$meta->{revision}///\n";
+
+            # permissions
+            $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
+            print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
+
+            # transmit file
+            transmitfile($meta->{filehash});
+        } else {
+            my ( $filepart, $dirpart ) = filenamesplit($meta->{name});
+
+            my $dir = tempdir( DIR => $TEMP_DIR, CLEANUP => 1 ) . "/";
+
+            chdir $dir;
+            my $file_local = $filepart . ".mine";
+            system("ln","-s",$state->{entries}{$filename}{modified_filename}, $file_local);
+            my $file_old = $filepart . "." . $oldmeta->{revision};
+            transmitfile($oldmeta->{filehash}, $file_old);
+            my $file_new = $filepart . "." . $meta->{revision};
+            transmitfile($meta->{filehash}, $file_new);
+
+            # we need to merge with the local changes ( M=successful merge, C=conflict merge )
+            $log->info("Merging $file_local, $file_old, $file_new");
+
+            $log->debug("Temporary directory for merge is $dir");
+
+            my $return = system("merge", $file_local, $file_old, $file_new);
+            $return >>= 8;
+
+            if ( $return == 0 )
+            {
+                $log->info("Merged successfully");
+                print "M M $filename\n";
+                $log->debug("Update-existing $dirpart");
+                print "Update-existing $dirpart\n";
+                $log->debug($state->{CVSROOT} . "/$state->{module}/$filename");
+                print $state->{CVSROOT} . "/$state->{module}/$filename\n";
+                $log->debug("/$filepart/1.$meta->{revision}///");
+                print "/$filepart/1.$meta->{revision}///\n";
+            }
+            elsif ( $return == 1 )
+            {
+                $log->info("Merged with conflicts");
+                print "M C $filename\n";
+                print "Update-existing $dirpart\n";
+                print $state->{CVSROOT} . "/$state->{module}/$filename\n";
+                print "/$filepart/1.$meta->{revision}/+//\n";
+            }
+            else
+            {
+                $log->warn("Merge failed");
+                next;
+            }
+
+            # permissions
+            $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
+            print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
+
+            # transmit file, format is single integer on a line by itself (file
+            # size) followed by the file contents
+            # TODO : we should copy files in blocks
+            my $data = `cat $file_local`;
+            $log->debug("File size : " . length($data));
+            print length($data) . "\n";
+            print $data;
+
+            chdir "/";
+        }
+
+    }
+
+    print "ok\n";
+}
+
+sub req_ci
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit("ci");
+
+    #$log->debug("State : " . Dumper($state));
+
+    $log->info("req_ci : " . ( defined($data) ? $data : "[NULL]" ));
+
+    if ( -e $state->{CVSROOT} . "/index" )
+    {
+        print "error 1 Index already exists in git repo\n";
+        exit;
+    }
+
+    my $lockfile = "$state->{CVSROOT}/refs/heads/$state->{module}.lock";
+    unless ( sysopen(LOCKFILE,$lockfile,O_EXCL|O_CREAT|O_WRONLY) )
+    {
+        print "error 1 Lock file '$lockfile' already exists, please try again\n";
+        exit;
+    }
+
+    # Grab a handle to the SQLite db and do any necessary updates
+    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+    $updater->update();
+
+    my $tmpdir = tempdir ( DIR => $TEMP_DIR );
+    my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
+    $log->info("Lock successful, basing commit on '$tmpdir', index file is '$file_index'");
+
+    $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
+    $ENV{GIT_INDEX_FILE} = $file_index;
+
+    chdir $tmpdir;
+
+    # populate the temporary index based
+    system("git-read-tree", $state->{module});
+    unless ($? == 0)
+    {
+       die "Error running git-read-tree $state->{module} $file_index $!";
+    }
+    $log->info("Created index '$file_index' with for head $state->{module} - exit status $?");
+
+
+    my @committedfiles = ();
+
+    # foreach file specified on the commandline ...
+    foreach my $filename ( @{$state->{args}} )
+    {
+        $filename = filecleanup($filename);
+
+        next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
+
+        my $meta = $updater->getmeta($filename);
+
+        my $wrev = revparse($filename);
+
+        my ( $filepart, $dirpart ) = filenamesplit($filename);
+
+        # do a checkout of the file if it part of this tree
+        if ($wrev) {
+            system('git-checkout-index', '-f', '-u', $filename);
+            unless ($? == 0) {
+                die "Error running git-checkout-index -f -u $filename : $!";
+            }
+        }
+
+        my $addflag = 0;
+        my $rmflag = 0;
+        $rmflag = 1 if ( defined($wrev) and $wrev < 0 );
+        $addflag = 1 unless ( -e $filename );
+
+        # Do up to date checking
+        unless ( $addflag or $wrev == $meta->{revision} or ( $rmflag and -$wrev == $meta->{revision} ) )
+        {
+            # fail everything if an up to date check fails
+            print "error 1 Up to date check failed for $filename\n";
+            close LOCKFILE;
+            unlink($lockfile);
+            chdir "/";
+            exit;
+        }
+
+        push @committedfiles, $filename;
+        $log->info("Committing $filename");
+
+        system("mkdir","-p",$dirpart) unless ( -d $dirpart );
+
+        unless ( $rmflag )
+        {
+            $log->debug("rename $state->{entries}{$filename}{modified_filename} $filename");
+            rename $state->{entries}{$filename}{modified_filename},$filename;
+
+            # Calculate modes to remove
+            my $invmode = "";
+            foreach ( qw (r w x) ) { $invmode .= $_ unless ( $state->{entries}{$filename}{modified_mode} =~ /$_/ ); }
+
+            $log->debug("chmod u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode . " $filename");
+            system("chmod","u+" .  $state->{entries}{$filename}{modified_mode} . "-" . $invmode, $filename);
+        }
+
+        if ( $rmflag )
+        {
+            $log->info("Removing file '$filename'");
+            unlink($filename);
+            system("git-update-index", "--remove", $filename);
+        }
+        elsif ( $addflag )
+        {
+            $log->info("Adding file '$filename'");
+            system("git-update-index", "--add", $filename);
+        } else {
+            $log->info("Updating file '$filename'");
+            system("git-update-index", $filename);
+        }
+    }
+
+    unless ( scalar(@committedfiles) > 0 )
+    {
+        print "E No files to commit\n";
+        print "ok\n";
+        close LOCKFILE;
+        unlink($lockfile);
+        chdir "/";
+        return;
+    }
+
+    my $treehash = `git-write-tree`;
+    my $parenthash = `cat $ENV{GIT_DIR}refs/heads/$state->{module}`;
+    chomp $treehash;
+    chomp $parenthash;
+
+    $log->debug("Treehash : $treehash, Parenthash : $parenthash");
+
+    # write our commit message out if we have one ...
+    my ( $msg_fh, $msg_filename ) = tempfile( DIR => $TEMP_DIR );
+    print $msg_fh $state->{opt}{m};# if ( exists ( $state->{opt}{m} ) );
+    print $msg_fh "\n\nvia git-CVS emulator\n";
+    close $msg_fh;
+
+    my $commithash = `git-commit-tree $treehash -p $parenthash < $msg_filename`;
+    $log->info("Commit hash : $commithash");
+
+    unless ( $commithash =~ /[a-zA-Z0-9]{40}/ )
+    {
+        $log->warn("Commit failed (Invalid commit hash)");
+        print "error 1 Commit failed (unknown reason)\n";
+        close LOCKFILE;
+        unlink($lockfile);
+        chdir "/";
+        exit;
+    }
+
+    open FILE, ">", "$ENV{GIT_DIR}refs/heads/$state->{module}";
+    print FILE $commithash;
+    close FILE;
+
+    $updater->update();
+
+    # foreach file specified on the commandline ...
+    foreach my $filename ( @committedfiles )
+    {
+        $filename = filecleanup($filename);
+
+        my $meta = $updater->getmeta($filename);
+
+        my ( $filepart, $dirpart ) = filenamesplit($filename);
+
+        $log->debug("Checked-in $dirpart : $filename");
+
+        if ( $meta->{filehash} eq "deleted" )
+        {
+            print "Remove-entry $dirpart\n";
+            print "$filename\n";
+        } else {
+            print "Checked-in $dirpart\n";
+            print "$filename\n";
+            print "/$filepart/1.$meta->{revision}///\n";
+        }
+    }
+
+    close LOCKFILE;
+    unlink($lockfile);
+    chdir "/";
+
+    print "ok\n";
+}
+
+sub req_status
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit("status");
+
+    $log->info("req_status : " . ( defined($data) ? $data : "[NULL]" ));
+    #$log->debug("status state : " . Dumper($state));
+
+    # Grab a handle to the SQLite db and do any necessary updates
+    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+    $updater->update();
+
+    # if no files were specified, we need to work out what files we should be providing status on ...
+    argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
+
+    # foreach file specified on the commandline ...
+    foreach my $filename ( @{$state->{args}} )
+    {
+        $filename = filecleanup($filename);
+
+        my $meta = $updater->getmeta($filename);
+        my $oldmeta = $meta;
+
+        my $wrev = revparse($filename);
+
+        # If the working copy is an old revision, lets get that version too for comparison.
+        if ( defined($wrev) and $wrev != $meta->{revision} )
+        {
+            $oldmeta = $updater->getmeta($filename, $wrev);
+        }
+
+        # TODO : All possible statuses aren't yet implemented
+        my $status;
+        # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified
+        $status = "Up-to-date" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision}
+                                    and
+                                    ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
+                                      or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta->{filehash} ) )
+                                   );
+
+        # Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified
+        $status ||= "Needs Checkout" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev
+                                          and
+                                          ( $state->{entries}{$filename}{unchanged}
+                                            or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} ) )
+                                        );
+
+        # Need checkout if it exists in the repo but doesn't have a working copy
+        $status ||= "Needs Checkout" if ( not defined ( $wrev ) and defined ( $meta->{revision} ) );
+
+        # Locally modified if working copy and repo copy have the same revision but there are local changes
+        $status ||= "Locally Modified" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{modified_filename} );
+
+        # Needs Merge if working copy revision is less than repo copy and there are local changes
+        $status ||= "Needs Merge" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev and $state->{entries}{$filename}{modified_filename} );
+
+        $status ||= "Locally Added" if ( defined ( $state->{entries}{$filename}{revision} ) and not defined ( $meta->{revision} ) );
+        $status ||= "Locally Removed" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and -$wrev == $meta->{revision} );
+        $status ||= "Unresolved Conflict" if ( defined ( $state->{entries}{$filename}{conflict} ) and $state->{entries}{$filename}{conflict} =~ /^\+=/ );
+        $status ||= "File had conflicts on merge" if ( 0 );
+
+        $status ||= "Unknown";
+
+        print "M ===================================================================\n";
+        print "M File: $filename\tStatus: $status\n";
+        if ( defined($state->{entries}{$filename}{revision}) )
+        {
+            print "M Working revision:\t" . $state->{entries}{$filename}{revision} . "\n";
+        } else {
+            print "M Working revision:\tNo entry for $filename\n";
+        }
+        if ( defined($meta->{revision}) )
+        {
+            print "M Repository revision:\t1." . $meta->{revision} . "\t$state->{repository}/$filename,v\n";
+            print "M Sticky Tag:\t\t(none)\n";
+            print "M Sticky Date:\t\t(none)\n";
+            print "M Sticky Options:\t\t(none)\n";
+        } else {
+            print "M Repository revision:\tNo revision control file\n";
+        }
+        print "M\n";
+    }
+
+    print "ok\n";
+}
+
+sub req_diff
+{
+    my ( $cmd, $data ) = @_;
+
+    argsplit("diff");
+
+    $log->debug("req_diff : " . ( defined($data) ? $data : "[NULL]" ));
+    #$log->debug("status state : " . Dumper($state));
+
+    my ($revision1, $revision2);
+    if ( defined ( $state->{opt}{r} ) and ref $state->{opt}{r} eq "ARRAY" )
+    {
+        $revision1 = $state->{opt}{r}[0];
+        $revision2 = $state->{opt}{r}[1];
+    } else {
+        $revision1 = $state->{opt}{r};
+    }
+
+    $revision1 =~ s/^1\.// if ( defined ( $revision1 ) );
+    $revision2 =~ s/^1\.// if ( defined ( $revision2 ) );
+
+    $log->debug("Diffing revisions " . ( defined($revision1) ? $revision1 : "[NULL]" ) . " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
+
+    # Grab a handle to the SQLite db and do any necessary updates
+    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+    $updater->update();
+
+    # if no files were specified, we need to work out what files we should be providing status on ...
+    argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
+
+    # foreach file specified on the commandline ...
+    foreach my $filename ( @{$state->{args}} )
+    {
+        $filename = filecleanup($filename);
+
+        my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
+
+        my $wrev = revparse($filename);
+
+        # We need _something_ to diff against
+        next unless ( defined ( $wrev ) );
+
+        # if we have a -r switch, use it
+        if ( defined ( $revision1 ) )
+        {
+            ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+            $meta1 = $updater->getmeta($filename, $revision1);
+            unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
+            {
+                print "E File $filename at revision 1.$revision1 doesn't exist\n";
+                next;
+            }
+            transmitfile($meta1->{filehash}, $file1);
+        }
+        # otherwise we just use the working copy revision
+        else
+        {
+            ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+            $meta1 = $updater->getmeta($filename, $wrev);
+            transmitfile($meta1->{filehash}, $file1);
+        }
+
+        # if we have a second -r switch, use it too
+        if ( defined ( $revision2 ) )
+        {
+            ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+            $meta2 = $updater->getmeta($filename, $revision2);
+
+            unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
+