Merge fixes up to GIT 1.2.2
authorJunio C Hamano <junkio@cox.net>
Sun, 19 Feb 2006 06:55:42 +0000 (22:55 -0800)
committerJunio C Hamano <junkio@cox.net>
Sun, 19 Feb 2006 06:55:42 +0000 (22:55 -0800)
30 files changed:
Documentation/SubmittingPatches
Documentation/git-commit.txt
Documentation/git-send-email.txt
Documentation/git-svnimport.txt
Documentation/git-tag.txt
Makefile
cache.h
commit.c
commit.h
contrib/README [new file with mode: 0644]
contrib/emacs/git.el [new file with mode: 0644]
contrib/git-svn/git-svn [new file with mode: 0755]
contrib/git-svn/git-svn.txt [new file with mode: 0644]
contrib/gitview/gitview [new file with mode: 0755]
contrib/gitview/gitview.txt [new file with mode: 0644]
git-add.sh
git-bisect.sh
git-commit.sh
git-rebase.sh
git-send-email.perl
git-svnimport.perl
git-tag.sh
ls-files.c
mailinfo.c
pack-check.c
rev-list.c
rev-parse.c
sha1_file.c
show-branch.c
templates/hooks--pre-rebase [new file with mode: 0644]

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.
 
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..5c543d5 100644 (file)
@@ -61,6 +61,10 @@ 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.
+
 -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 648469e..317be3c 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -53,6 +53,8 @@ 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 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.
@@ -380,6 +382,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
diff --git a/cache.h b/cache.h
index c255421..b5db01f 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -322,7 +322,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 67e11d7..c550a00 100644 (file)
--- a/commit.c
+++ b/commit.c
@@ -571,7 +571,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;
@@ -630,7 +630,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;
@@ -647,8 +650,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 */
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/git-svn b/contrib/git-svn/git-svn
new file mode 100755 (executable)
index 0000000..71a8b3b
--- /dev/null
@@ -0,0 +1,770 @@
+#!/usr/bin/env perl
+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.9.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.
+# See what I do with XML::Simple to make the dependency optional.
+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//;
+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);
+
+GetOptions(    'revision|r=s' => \$_revision,
+               'no-ignore-externals' => \$_no_ignore_ext,
+               'stdin|' => \$_stdin,
+               'edit|e' => \$_edit,
+               'rmdir' => \$_rmdir,
+               'help|H|h' => \$_help,
+               'no-stop-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" ],
+       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;
+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 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;
+
+       eval { require XML::Simple or croak $! };
+       my $svn_log = $@ ? svn_log_raw(@log_args) : svn_log_xml(@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 (/^([a-f\d]{6,40})\b/) {
+                               unshift @commits, $1;
+                       }
+               }
+       }
+       my @revs;
+       foreach (@commits) {
+               push @revs, (safe_qx('git-rev-parse',$_));
+       }
+       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";
+               svn_checkout_tree($svn_current_rev, $c);
+               $svn_current_rev = svn_commit_tree($svn_current_rev, $c);
+       }
+       print "Done committing ",scalar @revs," revisions to SVN\n";
+
+}
+
+########################### 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, $commit) = @_;
+       croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/);
+       croak "$commit is not a sha1!\n" unless ($commit =~ /^$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;
+       }
+       my ($tree_a) = grep(/^tree $sha1$/o,`git-cat-file commit $commit`);
+       $tree_a =~ s/^tree //;
+       chomp $tree_a;
+       chomp(my $tree_b = `GIT_INDEX_FILE=$GIT_SVN_INDEX git-write-tree`);
+       if ($tree_a ne $tree_b) {
+               croak "$svn_rev != $commit, $tree_a != $tree_b\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"
+               print $_,"\n";
+               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 __LINE__,": Empty array\n";
+                       if ($x->{chg} !~ /^(?:C|R)$/) {
+                               croak __LINE__,": Error parsing $_, $x->{chg}\n";
+                       }
+                       $x->{file_a} = $_;
+                       $state = 'file_b';
+               } elsif ($state eq 'file_b') {
+                       my $x = $mods[$#mods] or croak __LINE__,": Empty array\n";
+                       if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
+                               croak __LINE__,": Error parsing $_, $x->{chg}\n";
+                       }
+                       if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
+                               croak __LINE__,": Error parsing $_, $x->{chg}\n";
+                       }
+                       $x->{file_b} = $_;
+                       $state = 'meta';
+               } else {
+                       croak __LINE__,": Error parsing $_\n";
+               }
+       }
+       close $diff_fh or croak $!;
+       return \@mods;
+}
+
+sub svn_check_prop_executable {
+       my $m = shift;
+       if ($m->{mode_b} =~ /755$/ && $m->{mode_a} !~ /755$/) {
+               sys(qw(svn propset svn:executable 1), $m->{file_b});
+       } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
+               sys(qw(svn propdel svn:executable), $m->{file_b});
+       }
+}
+
+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 svn_checkout_tree {
+       my ($svn_rev, $commit) = @_;
+       my $from = file_to_s("$REV_DIR/$svn_rev");
+       assert_svn_wc_clean($svn_rev,$from);
+       print "diff-tree '$from' '$commit'\n";
+       my $pid = open my $diff_fh, '-|';
+       defined $pid or croak $!;
+       if ($pid == 0) {
+               exec(qw(git-diff-tree -z -r -C), $from, $commit) or croak $!;
+       }
+       my $mods = parse_diff_tree($diff_fh);
+       unless (@$mods) {
+               # git can do empty commits, SVN doesn't allow it...
+               return $svn_rev;
+       }
+       my %rm;
+       foreach my $m (@$mods) {
+               if ($m->{chg} eq 'C') {
+                       svn_ensure_parent_path( $m->{file_b} );
+                       sys(qw(svn cp),         $m->{file_a}, $m->{file_b});
+                       blob_to_file(           $m->{sha1_b}, $m->{file_b});
+                       svn_check_prop_executable($m);
+               } elsif ($m->{chg} eq 'D') {
+                       $rm{dirname $m->{file_b}}->{basename $m->{file_b}} = 1;
+                       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});
+                       blob_to_file(           $m->{sha1_b}, $m->{file_b});
+                       svn_check_prop_executable($m);
+                       $rm{dirname $m->{file_a}}->{basename $m->{file_a}} = 1;
+               } elsif ($m->{chg} eq 'M') {
+                       if ($m->{mode_b} =~ /^120/ && $m->{mode_a} =~ /^120/) {
+                               unlink $m->{file_b} or croak $!;
+                               blob_to_symlink($m->{sha1_b}, $m->{file_b});
+                       } else {
+                               blob_to_file($m->{sha1_b}, $m->{file_b});
+                       }
+                       svn_check_prop_executable($m);
+               } elsif ($m->{chg} eq 'T') {
+                       sys(qw(svn rm --force),$m->{file_b});
+                       if ($m->{mode_b} =~ /^120/ && $m->{mode_a} =~ /^100/) {
+                               blob_to_symlink($m->{sha1_b}, $m->{file_b});
+                       } else {
+                               blob_to_file($m->{sha1_b}, $m->{file_b});
+                       }
+                       svn_check_prop_executable($m);
+                       sys(qw(svn add --force), $m->{file_b});
+               } elsif ($m->{chg} eq 'A') {
+                       svn_ensure_parent_path( $m->{file_b} );
+                       blob_to_file(           $m->{sha1_b}, $m->{file_b});
+                       if ($m->{mode_b} =~ /755$/) {
+                               chmod 0755, $m->{file_b};
+                       }
+                       sys(qw(svn add --force), $m->{file_b});
+               } else {
+                       croak "Invalid chg: $m->{chg}\n";
+               }
+       }
+       if ($_rmdir) {
+               my $old_index = $ENV{GIT_INDEX_FILE};
+               $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
+               foreach my $dir (keys %rm) {
+                       my $files = $rm{$dir};
+                       my @files;
+                       foreach (safe_qx('svn','ls',$dir)) {
+                               chomp;
+                               push @files, $_ unless $files->{$_};
+                       }
+                       sys(qw(svn rm),$dir) unless @files;
+               }
+               if ($old_index) {
+                       $ENV{GIT_INDEX_FILE} = $old_index;
+               } else {
+                       delete $ENV{GIT_INDEX_FILE};
+               }
+       }
+}
+
+sub svn_commit_tree {
+       my ($svn_rev, $commit) = @_;
+       my $commit_msg = "$GIT_DIR/$GIT_SVN/.svn-commit.tmp.$$";
+       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 {
+                               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+)\./);
+
+       # resync immediately
+       my @svn_up = (qw(svn up), "-r$svn_rev");
+       push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
+       sys(@svn_up);
+       return fetch("$rev_committed=$commit")->{revision};
+}
+
+sub svn_log_xml {
+       my (@log_args) = @_;
+       my $log_fh = IO::File->new_tmpfile or croak $!;
+
+       my $pid = fork;
+       defined $pid or croak $!;
+
+       if ($pid == 0) {
+               open STDOUT, '>&', $log_fh or croak $!;
+               exec (qw(svn log --xml), @log_args) or croak $!
+       }
+
+       waitpid $pid, 0;
+       croak $? if $?;
+
+       seek $log_fh, 0, 0;
+       my @svn_log;
+       my $log = XML::Simple::XMLin( $log_fh,
+                               ForceArray => ['path','revision','logentry'],
+                               KeepRoot => 0,
+                               KeyAttr => {    logentry => '+revision',
+                                               paths => '+path' },
+                       )->{logentry};
+       foreach my $r (sort {$a <=> $b} keys %$log) {
+               my $log_msg = $log->{$r};
+               my ($Y,$m,$d,$H,$M,$S) = ($log_msg->{date} =~
+                                       /(\d{4})\-(\d\d)\-(\d\d)T
+                                        (\d\d)\:(\d\d)\:(\d\d)\.\d+Z$/x)
+                                        or croak "Failed to parse date: ",
+                                                $log->{$r}->{date};
+               $log_msg->{date} = "+0000 $Y-$m-$d $H:$M:$S";
+
+               # XML::Simple can't handle <msg></msg> as a string:
+               if (ref $log_msg->{msg} eq 'HASH') {
+                       $log_msg->{msg} = "\n";
+               } else {
+                       $log_msg->{msg} .= "\n";
+               }
+               push @svn_log, $log->{$r};
+       }
+       return \@svn_log;
+}
+
+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;
+       while (<$log_fh>) {
+               chomp;
+               if (/^\-{72}$/) {
+                       $state = 'rev';
+
+                       # if we have an empty log message, put something there:
+                       if (@svn_log) {
+                               $svn_log[$#svn_log]->{msg} ||= "\n";
+                       }
+                       next;
+               }
+               if ($state eq 'rev' && s/^r(\d+)\s*\|\s*//) {
+                       my $rev = $1;
+                       my ($author, $date) = split(/\s*\|\s*/, $_, 2);
+                       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,
+                                       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') {
+                       $svn_log[$#svn_log]->{msg} .= $_."\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 blob_to_symlink {
+       my ($blob, $link) = @_;
+       defined $link or croak "\$link not defined!\n";
+       croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
+       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;
+       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..4b79fb0
--- /dev/null
@@ -0,0 +1,208 @@
+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.
+
+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.
+
+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.
+
+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 rev-list --pretty=oneline git-svn-HEAD..my-branch | git-svn commit
+# Something is committed to SVN, pull the latest into your branch::
+       git-svn fetch && git pull . git-svn-HEAD
+
+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.
+
+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).
+
+Author
+------
+Written by Eric Wong <normalperson@yhbt.net>.
+
+Documentation
+-------------
+Written by Eric Wong <normalperson@yhbt.net>.
diff --git a/contrib/gitview/gitview b/contrib/gitview/gitview
new file mode 100755 (executable)
index 0000000..5862fcc
--- /dev/null
@@ -0,0 +1,992 @@
+#! /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))
+
+def get_sha1_from_tags(line):
+       fp = os.popen("git cat-file -t " + line)
+       entry = string.strip(fp.readline())
+       fp.close()
+       if (entry == "commit"):
+               return line
+       elif (entry == "tag"):
+               fp = os.popen("git cat-file tag "+ line)
+               entry = string.strip(fp.readline())
+               fp.close()
+               obj = re.split(" ", entry)
+               if (obj[0] == "object"):
+                       return obj[1]
+       return None
+
+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 = max(cols, start, end)
+
+               (column, colour, names) = self.node
+               names_len = 0
+               if (len(names) != 0):
+                       for item in names:
+                               names_len += len(item)/3
+
+               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)
+
+
+               if (len(names) != 0):
+                       name = " "
+                       for item in names:
+                               name = name + item + " "
+
+                       ctx.text_path(name)
+
+               self.set_colour(ctx, colour, 0.0, 0.5)
+               ctx.stroke_preserve()
+
+               self.set_colour(ctx, colour, 0.5, 1.0)
+               ctx.fill()
+
+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, False, False, 2)
+               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):
+               """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(fp.read())
+               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.6"
+
+       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_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 = { }
+               git_dir = os.getenv("GIT_DIR")
+               if (git_dir == None):
+                       git_dir = ".git"
+
+               #FIXME the path seperator
+               ref_files = os.listdir(git_dir + "/refs/tags")
+               for file in ref_files:
+                       fp = open(git_dir + "/refs/tags/"+file)
+                       sha1 = get_sha1_from_tags(string.strip(fp.readline()))
+                       try:
+                               self.bt_sha1[sha1].append(file)
+                       except KeyError:
+                               self.bt_sha1[sha1] = [file]
+                       fp.close()
+
+
+               #FIXME the path seperator
+               ref_files = os.listdir(git_dir + "/refs/heads")
+               for file in ref_files:
+                       fp = open(git_dir + "/refs/heads/" + file)
+                       sha1 = get_sha1_from_tags(string.strip(fp.readline()))
+                       try:
+                               self.bt_sha1[sha1].append(file)
+                       except KeyError:
+                               self.bt_sha1[sha1] = [file]
+                       fp.close()
+
+
+       def construct(self):
+               """Construct the window contents."""
+               paned = gtk.VPaned()
+               paned.pack1(self.construct_top(), resize=False, shrink=True)
+               paned.pack2(self.construct_bottom(), resize=False, shrink=True)
+               self.window.add(paned)
+               paned.show()
+
+
+       def construct_top(self):
+               """Construct the top-half of the window."""
+               vbox = gtk.VBox(spacing=6)
+               vbox.set_border_width(12)
+               vbox.show()
+
+               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)
+               vbox.pack_start(menu_bar, False, False, 2)
+               menu_bar.show()
+
+               scrollwin = gtk.ScrolledWindow()
+               scrollwin.set_policy(gtk.POLICY_NEVER, 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(False)
+               column.pack_start(cell, expand=False)
+               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(message)
+
+               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)
+                       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 = {}
+
+               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 ):
+                               (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
+
+                       # Skip the '\0
+                       commit_lines = []
+                       commit_lines.append(input_line[1:])
+                       input_line = fp.readline()
+
+               fp.close()
+
+               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 = 0
+
+               # Add the incomplete lines of the last cell in this
+               for sha1 in self.incomplete_line.keys():
+                       if ( sha1 != commit.commit_sha1):
+                               for pos in self.incomplete_line[sha1]:
+                                       in_line.append((pos, pos, self.colours[sha1]))
+                       else:
+                               del self.incomplete_line[sha1]
+
+               try:
+                       colour = self.colours[commit.commit_sha1]
+               except KeyError:
+                       last_colour +=1
+                       self.colours[commit.commit_sha1] = last_colour
+                       colour =  last_colour
+               try:
+                       node_pos = self.nodepos[commit.commit_sha1]
+               except KeyError:
+                       last_nodepos +=1
+                       self.nodepos[commit.commit_sha1] = last_nodepos
+                       node_pos = last_nodepos
+
+               #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
+
+               in_line.append((node_pos, self.nodepos[commit.parent_sha1[0]],
+                                       self.colours[commit.parent_sha1[0]]))
+
+               self.add_incomplete_line(commit.parent_sha1[0], index+1)
+
+               if (len(commit.parent_sha1) > 1):
+                       for parent_id in commit.parent_sha1[1:]:
+                               try:
+                                       tmp_node_pos = self.nodepos[parent_id]
+                               except KeyError:
+                                       last_colour += 1;
+                                       self.colours[parent_id] = last_colour
+                                       last_nodepos +=1
+                                       self.nodepos[parent_id] = last_nodepos
+
+                               in_line.append((node_pos, self.nodepos[parent_id],
+                                                       self.colours[parent_id]))
+                               self.add_incomplete_line(parent_id, index+1)
+
+
+               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, index):
+               try:
+                       self.incomplete_line[sha1].append(self.nodepos[sha1])
+               except KeyError:
+                       self.incomplete_line[sha1] = [self.nodepos[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):
+               """Callback for when the show button for a parent is clicked."""
+               window = DiffWindow()
+               window.set_diff(commit_sha1, parent_sha1)
+               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 f719b4b..13fad82 100755 (executable)
@@ -24,6 +24,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 \
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 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 16d4359..21c3d83 100755 (executable)
@@ -3,10 +3,48 @@
 # Copyright (c) 2005 Junio C Hamano.
 #
 
-USAGE='<upstream> [<head>]'
+USAGE='[--onto <newbase>] <upstream> [<branch>]'
+LONG_USAGE='If <branch> is specified, switch to that branch first.  Then,
+extract commits in the current branch that are not in <upstream>,
+and reconstruct the current on top of <upstream>, discarding the original
+development history.  If --onto <newbase> is specified, the history is
+reconstructed on top of <newbase>, instead of <upstream>.  For example,
+while on "topic" branch:
+
+          A---B---C topic
+         /
+    D---E---F---G master
+
+       $ '"$0"' --onto master~1 master topic
+
+would rewrite the history to look like this:
+
+
+             A'\''--B'\''--C'\'' topic
+            /
+    D---E---F---G master
+'
+
 . git-sh-setup
 
-case $# in 1|2) ;; *) usage ;; esac
+unset newbase
+while case "$#" in 0) break ;; esac
+do
+       case "$1" in
+       --onto)
+               test 2 -le "$#" || usage
+               newbase="$2"
+               shift
+               ;;
+       -*)
+               usage
+               ;;
+       *)
+               break
+               ;;
+       esac
+       shift
+done
 
 # Make sure we do not have .dotest
 if mkdir .dotest
@@ -30,37 +68,61 @@ case "$diff" in
        ;;
 esac
 
-# The other head is given.  Make sure it is valid.
-other=$(git-rev-parse --verify "$1^0") || usage
+# The upstream head must be given.  Make sure it is valid.
+upstream_name="$1"
+upstream=`git rev-parse --verify "${upstream_name}^0"` ||
+    die "invalid upsteram $upstream_name"
 
-# Make sure the branch to rebase is valid.
-head=$(git-rev-parse --verify "${2-HEAD}^0") || exit
+# If a hook exists, give it a chance to interrupt
+if test -x "$GIT_DIR/hooks/pre-rebase"
+then
+       "$GIT_DIR/hooks/pre-rebase" ${1+"$@"} || {
+               echo >&2 "The pre-rebase hook refused to rebase."
+               exit 1
+       }
+fi
 
 # If the branch to rebase is given, first switch to it.
 case "$#" in
 2)
+       branch_name="$2"
        git-checkout "$2" || usage
+       ;;
+*)
+       branch_name=`git symbolic-ref HEAD` || die "No current branch"
+       branch_name=`expr "$branch_name" : 'refs/heads/\(.*\)'`
+       ;;
 esac
+branch=$(git-rev-parse --verify "${branch_name}^0") || exit
+
+# Make sure the branch to rebase onto is valid.
+onto_name=${newbase-"$upstream_name"}
+onto=$(git-rev-parse --verify "${onto_name}^0") || exit
 
-mb=$(git-merge-base "$other" "$head")
+# Now we are rebasing commits $upstream..$branch on top of $onto
 
-# Check if we are already based on $other.
-if test "$mb" = "$other"
+# Check if we are already based on $onto, but this should be
+# done only when upstream and onto are the same.
+if test "$upstream" = "onto"
 then
-       echo >&2 "Current branch `git-symbolic-ref HEAD` is up to date."
-       exit 0
+       mb=$(git-merge-base "$onto" "$branch")
+       if test "$mb" = "$onto"
+       then
+               echo >&2 "Current branch $branch_name is up to date."
+               exit 0
+       fi
 fi
 
-# Rewind the head to "$other"
-git-reset --hard "$other"
+# Rewind the head to "$onto"; this saves our current head in ORIG_HEAD.
+git-reset --hard "$onto"
 
-# If the $other is a proper descendant of the tip of the branch, then
+# If the $onto is a proper descendant of the tip of the branch, then
 # we just fast forwarded.
-if test "$mb" = "$head"
+if test "$mb" = "$onto"
 then
-       echo >&2 "Fast-forwarded $head to $other."
+       echo >&2 "Fast-forwarded $branch to $newbase."
        exit 0
 fi
 
-git-format-patch -k --stdout --full-index "$other" ORIG_HEAD |
+git-format-patch -k --stdout --full-index "$upstream" ORIG_HEAD |
 git am --binary -3 -k
index 3f1b3ca..13b85dd 100755 (executable)
@@ -31,10 +31,10 @@ sub cleanup_compose_files();
 my $compose_filename = ".msg.$$";
 
 # Variables we fill in automatically, or via prompting:
-my (@to,@cc,$initial_reply_to,$initial_subject,@files,$from,$compose);
+my (@to,@cc,@initial_cc,$initial_reply_to,$initial_subject,@files,$from,$compose);
 
 # Behavior modification variables
-my ($chain_reply_to, $smtp_server, $quiet) = (1, "localhost", 0);
+my ($chain_reply_to, $smtp_server, $quiet, $suppress_from, $no_signed_off_cc) = (1, "localhost", 0, 0, 0);
 
 # Example reply to:
 #$initial_reply_to = ''; #<20050203173208.GA23964@foobar.com>';
@@ -48,10 +48,13 @@ my $rc = GetOptions("from=s" => \$from,
                     "in-reply-to=s" => \$initial_reply_to,
                    "subject=s" => \$initial_subject,
                    "to=s" => \@to,
+                   "cc=s" => \@initial_cc,
                    "chain-reply-to!" => \$chain_reply_to,
                    "smtp-server=s" => \$smtp_server,
                    "compose" => \$compose,
                    "quiet" => \$quiet,
+                   "suppress-from" => \$suppress_from,
+                   "no-signed-off-cc" => \$no_signed_off_cc,
         );
 
 # Now, let's fill any that aren't set in with defaults:
@@ -197,6 +200,9 @@ Options:
 
    --to           Specify the primary "To:" line of the email.
 
+   --cc           Specify an initial "Cc:" list for the entire series
+                  of emails.
+
    --compose      Use \$EDITOR to edit an introductory message for the
                   patch series.
 
@@ -212,13 +218,19 @@ Options:
                   email sent, rather than to the first email sent.
                   Defaults to on.
 
+   --no-signed-off-cc Suppress the automatic addition of email addresses
+                 that appear in a Signed-off-by: line, to the cc: list.
+                Note: Using this option is not recommended.
+
    --smtp-server  If set, specifies the outgoing SMTP server to use.
                   Defaults to localhost.
 
+  --suppress-from Supress sending emails to yourself if your address
+                  appears in a From: line.
+
    --quiet     Make git-send-email less verbose.  One line per email should be
                all that is output.
 
-
 Error: Please specify a file or a directory on the command line.
 EOT
        exit(1);
@@ -290,7 +302,7 @@ $subject = $initial_subject;
 foreach my $t (@files) {
        open(F,"<",$t) or die "can't open file $t";
 
-       @cc = ();
+       @cc = @initial_cc;
        my $found_mbox = 0;
        my $header_done = 0;
        $message = "";
@@ -304,6 +316,7 @@ foreach my $t (@files) {
                                        $subject = $1;
 
                                } elsif (/^(Cc|From):\s+(.*)$/) {
+                                       next if ($2 eq $from && $suppress_from);
                                        printf("(mbox) Adding cc: %s from line '%s'\n",
                                                $2, $_) unless $quiet;
                                        push @cc, $2;
@@ -332,7 +345,7 @@ foreach my $t (@files) {
                        }
                } else {
                        $message .=  $_;
-                       if (/^Signed-off-by: (.*)$/i) {
+                       if (/^Signed-off-by: (.*)$/i && !$no_signed_off_cc) {
                                my $c = $1;
                                chomp $c;
                                push @cc, $c;
index f17d5a2..c536d70 100755 (executable)
@@ -30,19 +30,19 @@ die "Need SVN:Core 1.2.1 or better" if $SVN::Core::VERSION lt "1.2.1";
 $SIG{'PIPE'}="IGNORE";
 $ENV{'TZ'}="UTC";
 
-our($opt_h,$opt_o,$opt_v,$opt_u,$opt_C,$opt_i,$opt_m,$opt_M,$opt_t,$opt_T,$opt_b,$opt_s,$opt_l,$opt_d,$opt_D);
+our($opt_h,$opt_o,$opt_v,$opt_u,$opt_C,$opt_i,$opt_m,$opt_M,$opt_t,$opt_T,$opt_b,$opt_r,$opt_s,$opt_l,$opt_d,$opt_D);
 
 sub usage() {
        print STDERR <<END;
 Usage: ${\basename $0}     # fetch/update GIT from SVN
        [-o branch-for-HEAD] [-h] [-v] [-l max_rev]
        [-C GIT_repository] [-t tagname] [-T trunkname] [-b branchname]
-       [-d|-D] [-i] [-u] [-s start_chg] [-m] [-M regex] [SVN_URL]
+       [-d|-D] [-i] [-u] [-r] [-s start_chg] [-m] [-M regex] [SVN_URL]
 END
        exit(1);
 }
 
-getopts("b:C:dDhil:mM:o:s:t:T:uv") or usage();
+getopts("b:C:dDhil:mM:o:rs:t:T:uv") or usage();
 usage if $opt_h;
 
 my $tag_name = $opt_t || "tags";
@@ -650,6 +650,7 @@ sub commit {
                $pr->reader();
 
                $message =~ s/[\s\n]+\z//;
+               $message = "r$revision: $message" if $opt_r;
 
                print $pw "$message\n"
                        or die "Error writing to git-commit-tree: $!\n";
index c74e1b4..76e51ed 100755 (executable)
@@ -1,7 +1,7 @@
 #!/bin/sh
 # Copyright (c) 2005 Linus Torvalds
 
-USAGE='[-a | -s | -u <key-id>] [-f | -d] [-m <msg>] <tagname> [<head>]'
+USAGE='-l [<pattern>] | [-a | -s | -u <key-id>] [-f | -d] [-m <msg>] <tagname> [<head>]'
 SUBDIRECTORY_OK='Yes'
 . git-sh-setup
 
@@ -10,6 +10,7 @@ signed=
 force=
 message=
 username=
+list=
 while case "$#" in 0) break ;; esac
 do
     case "$1" in
@@ -23,6 +24,17 @@ do
     -f)
        force=1
        ;;
+    -l)
+        cd "$GIT_DIR/refs" &&
+       case "$#" in
+       1)
+               find tags -type f -print ;;
+       *)
+               shift
+               find tags -type f -print | grep "$@" ;;
+       esac
+       exit $?
+       ;;
     -m)
        annotate=1
        shift
index 7024cf1..df93cf2 100644 (file)
@@ -25,6 +25,8 @@ static int line_terminator = '\n';
 static int prefix_len = 0, prefix_offset = 0;
 static const char *prefix = NULL;
 static const char **pathspec = NULL;
+static int error_unmatch = 0;
+static char *ps_matched = NULL;
 
 static const char *tag_cached = "";
 static const char *tag_unmerged = "";
@@ -325,7 +327,8 @@ static int cmp_name(const void *p1, const void *p2)
  * Match a pathspec against a filename. The first "len" characters
  * are the common prefix
  */
-static int match(const char **spec, const char *filename, int len)
+static int match(const char **spec, char *ps_matched,
+                const char *filename, int len)
 {
        const char *m;
 
@@ -333,17 +336,24 @@ static int match(const char **spec, const char *filename, int len)
                int matchlen = strlen(m + len);
 
                if (!matchlen)
-                       return 1;
+                       goto matched;
                if (!strncmp(m + len, filename + len, matchlen)) {
                        if (m[len + matchlen - 1] == '/')
-                               return 1;
+                               goto matched;
                        switch (filename[len + matchlen]) {
                        case '/': case '\0':
-                               return 1;
+                               goto matched;
                        }
                }
                if (!fnmatch(m + len, filename + len, 0))
-                       return 1;
+                       goto matched;
+               if (ps_matched)
+                       ps_matched++;
+               continue;
+       matched:
+               if (ps_matched)
+                       *ps_matched = 1;
+               return 1;
        }
        return 0;
 }
@@ -356,7 +366,7 @@ static void show_dir_entry(const char *tag, struct nond_on_fs *ent)
        if (len >= ent->len)
                die("git-ls-files: internal error - directory entry not superset of prefix");
 
-       if (pathspec && !match(pathspec, ent->name, len))
+       if (pathspec && !match(pathspec, ps_matched, ent->name, len))
                return;
 
        fputs(tag, stdout);
@@ -444,7 +454,7 @@ static void show_ce_entry(const char *tag, struct cache_entry *ce)
        if (len >= ce_namelen(ce))
                die("git-ls-files: internal error - cache entry not superset of prefix");
 
-       if (pathspec && !match(pathspec, ce->name, len))
+       if (pathspec && !match(pathspec, ps_matched, ce->name, len))
                return;
 
        if (!show_stage) {
@@ -699,6 +709,10 @@ int main(int argc, const char **argv)
                        prefix_offset = 0;
                        continue;
                }
+               if (!strcmp(arg, "--error-unmatch")) {
+                       error_unmatch = 1;
+                       continue;
+               }
                if (*arg == '-')
                        usage(ls_files_usage);
                break;
@@ -710,6 +724,14 @@ int main(int argc, const char **argv)
        if (pathspec)
                verify_pathspec();
 
+       /* Treat unmatching pathspec elements as errors */
+       if (pathspec && error_unmatch) {
+               int num;
+               for (num = 0; pathspec[num]; num++)
+                       ;
+               ps_matched = xcalloc(1, num);
+       }
+
        if (show_ignored && !exc_given) {
                fprintf(stderr, "%s: --ignored needs some exclude pattern\n",
                        argv[0]);
@@ -725,5 +747,20 @@ int main(int argc, const char **argv)
        if (prefix)
                prune_cache();
        show_files();
+
+       if (ps_matched) {
+               /* We need to make sure all pathspec matched otherwise
+                * it is an error.
+                */
+               int num, errors = 0;
+               for (num = 0; pathspec[num]; num++) {
+                       if (ps_matched[num])
+                               continue;
+                       error("pathspec '%s' did not match any.",
+                             pathspec[num] + prefix_offset);
+               }
+               return errors ? 1 : 0;
+       }
+
        return 0;
 }
index ff2d4d4..3c56f8c 100644 (file)
@@ -7,7 +7,9 @@
 #include <stdlib.h>
 #include <string.h>
 #include <ctype.h>
+#ifndef NO_ICONV
 #include <iconv.h>
+#endif
 #include "git-compat-util.h"
 #include "cache.h"
 
@@ -469,6 +471,7 @@ static int decode_b_segment(char *in, char *ot, char *ep)
 
 static void convert_to_utf8(char *line, char *charset)
 {
+#ifndef NO_ICONV
        char *in, *out;
        size_t insize, outsize, nrc;
        char outbuf[4096]; /* cheat */
@@ -501,6 +504,7 @@ static void convert_to_utf8(char *line, char *charset)
                return;
        *out = 0;
        strcpy(line, outbuf);
+#endif
 }
 
 static void decode_header_bq(char *it)
index 67a7ecd..eca32b6 100644 (file)
@@ -84,7 +84,7 @@ static void show_pack_info(struct packed_git *p)
                char type[20];
                unsigned long size;
                unsigned long store_size;
-               int delta_chain_length;
+               unsigned int delta_chain_length;
 
                if (nth_packed_object_sha1(p, i, sha1))
                        die("internal error pack-check nth-packed-object");
@@ -98,7 +98,7 @@ static void show_pack_info(struct packed_git *p)
                if (!delta_chain_length)
                        printf("%-6s %lu %u\n", type, size, e.offset);
                else
-                       printf("%-6s %lu %u %d %s\n", type, size, e.offset,
+                       printf("%-6s %lu %u %u %s\n", type, size, e.offset,
                               delta_chain_length, sha1_to_hex(base_sha1));
        }
 
index 63391fc..f2d1105 100644 (file)
@@ -27,6 +27,7 @@ static const char rev_list_usage[] =
 "  ordering output:\n"
 "    --merge-order [ --show-breaks ]\n"
 "    --topo-order\n"
+"    --date-order\n"
 "  formatting output:\n"
 "    --parents\n"
 "    --objects\n"
@@ -56,6 +57,7 @@ static int merge_order = 0;
 static int show_breaks = 0;
 static int stop_traversal = 0;
 static int topo_order = 0;
+static int lifo = 1;
 static int no_merges = 0;
 static const char **paths = NULL;
 static int remove_empty_trees = 0;
@@ -856,6 +858,13 @@ int main(int argc, const char **argv)
                }
                if (!strcmp(arg, "--topo-order")) {
                        topo_order = 1;
+                       lifo = 1;
+                       limited = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--date-order")) {
+                       topo_order = 1;
+                       lifo = 0;
                        limited = 1;
                        continue;
                }
@@ -940,7 +949,7 @@ int main(int argc, const char **argv)
                if (limited)
                        list = limit_list(list);
                if (topo_order)
-                       sort_in_topological_order(&list);
+                       sort_in_topological_order(&list, lifo);
                show_commit_list(list);
        } else {
 #ifndef NO_OPENSSL
index 70a8271..a5fb93c 100644 (file)
@@ -48,6 +48,7 @@ static int is_rev_argument(const char *arg)
                "--show-breaks",
                "--sparse",
                "--topo-order",
+               "--date-order",
                "--unpacked",
                NULL
        };
index f08b1d6..1d799f7 100644 (file)
@@ -832,7 +832,7 @@ void packed_object_info_detail(struct pack_entry *e,
                               char *type,
                               unsigned long *size,
                               unsigned long *store_size,
-                              int *delta_chain_length,
+                              unsigned int *delta_chain_length,
                               unsigned char *base_sha1)
 {
        struct packed_git *p = e->p;
@@ -846,7 +846,7 @@ void packed_object_info_detail(struct pack_entry *e,
        if (kind != OBJ_DELTA)
                *delta_chain_length = 0;
        else {
-               int chain_length = 0;
+               unsigned int chain_length = 0;
                memcpy(base_sha1, pack, 20);
                do {
                        struct pack_entry base_ent;
index 511fd3b..5a86ae2 100644 (file)
@@ -535,6 +535,7 @@ int main(int ac, char **av)
        int num_rev, i, extra = 0;
        int all_heads = 0, all_tags = 0;
        int all_mask, all_revs;
+       int lifo = 1;
        char head_path[128];
        const char *head_path_p;
        int head_path_len;
@@ -544,7 +545,6 @@ int main(int ac, char **av)
        int no_name = 0;
        int sha1_name = 0;
        int shown_merge_point = 0;
-       int topo_order = 0;
        int with_current_branch = 0;
        int head_at = -1;
 
@@ -586,7 +586,9 @@ int main(int ac, char **av)
                else if (!strcmp(arg, "--independent"))
                        independent = 1;
                else if (!strcmp(arg, "--topo-order"))
-                       topo_order = 1;
+                       lifo = 1;
+               else if (!strcmp(arg, "--date-order"))
+                       lifo = 0;
                else
                        usage(show_branch_usage);
                ac--; av++;
@@ -710,8 +712,7 @@ int main(int ac, char **av)
                exit(0);
 
        /* Sort topologically */
-       if (topo_order)
-               sort_in_topological_order(&seen);
+       sort_in_topological_order(&seen, lifo);
 
        /* Give names to commits */
        if (!sha1_name && !no_name)
diff --git a/templates/hooks--pre-rebase b/templates/hooks--pre-rebase
new file mode 100644 (file)
index 0000000..981c454
--- /dev/null
@@ -0,0 +1,150 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+publish=next
+basebranch="$1"
+if test "$#" = 2
+then
+       topic="refs/heads/$2"
+else
+       topic=`git symbolic-ref HEAD`
+fi
+
+case "$basebranch,$topic" in
+master,refs/heads/??/*)
+       ;;
+*)
+       exit 0 ;# we do not interrupt others.
+       ;;
+esac
+
+# Now we are dealing with a topic branch being rebased
+# on top of master.  Is it OK to rebase it?
+
+# Is topic fully merged to master?
+not_in_master=`git-rev-list --pretty=oneline ^master "$topic"`
+if test -z "$not_in_master"
+then
+       echo >&2 "$topic is fully merged to master; better remove it."
+       exit 1 ;# we could allow it, but there is no point.
+fi
+
+# Is topic ever merged to next?  If so you should not be rebasing it.
+only_next_1=`git-rev-list ^master "^$topic" ${publish} | sort`
+only_next_2=`git-rev-list ^master           ${publish} | sort`
+if test "$only_next_1" = "$only_next_2"
+then
+       not_in_topic=`git-rev-list "^$topic" master`
+       if test -z "$not_in_topic"
+       then
+               echo >&2 "$topic is already up-to-date with master"
+               exit 1 ;# we could allow it, but there is no point.
+       else
+               exit 0
+       fi
+else
+       not_in_next=`git-rev-list --pretty=oneline ^${publish} "$topic"`
+       perl -e '
+               my $topic = $ARGV[0];
+               my $msg = "* $topic has commits already merged to public branch:\n";
+               my (%not_in_next) = map {
+                       /^([0-9a-f]+) /;
+                       ($1 => 1);
+               } split(/\n/, $ARGV[1]);
+               for my $elem (map {
+                               /^([0-9a-f]+) (.*)$/;
+                               [$1 => $2];
+                       } split(/\n/, $ARGV[2])) {
+                       if (!exists $not_in_next{$elem->[0]}) {
+                               if ($msg) {
+                                       print STDERR $msg;
+                                       undef $msg;
+                               }
+                               print STDERR " $elem->[1]\n";
+                       }
+               }
+       ' "$topic" "$not_in_next" "$not_in_master"
+       exit 1
+fi
+
+exit 0
+
+################################################################
+
+This sample hook safeguards topic branches that have been
+published from being rewound.
+
+The workflow assumed here is:
+
+ * Once a topic branch forks from "master", "master" is never
+   merged into it again (either directly or indirectly).
+
+ * Once a topic branch is fully cooked and merged into "master",
+   it is deleted.  If you need to build on top of it to correct
+   earlier mistakes, a new topic branch is created by forking at
+   the tip of the "master".  This is not strictly necessary, but
+   it makes it easier to keep your history simple.
+
+ * Whenever you need to test or publish your changes to topic
+   branches, merge them into "next" branch.
+
+The script, being an example, hardcodes the publish branch name
+to be "next", but it is trivial to make it configurable via
+$GIT_DIR/config mechanism.
+
+With this workflow, you would want to know:
+
+(1) ... if a topic branch has ever been merged to "next".  Young
+    topic branches can have stupid mistakes you would rather
+    clean up before publishing, and things that have not been
+    merged into other branches can be easily rebased without
+    affecting other people.  But once it is published, you would
+    not want to rewind it.
+
+(2) ... if a topic branch has been fully merged to "master".
+    Then you can delete it.  More importantly, you should not
+    build on top of it -- other people may already want to
+    change things related to the topic as patches against your
+    "master", so if you need further changes, it is better to
+    fork the topic (perhaps with the same name) afresh from the
+    tip of "master".
+
+Let's look at this example:
+
+                  o---o---o---o---o---o---o---o---o---o "next"
+                 /       /           /           /
+                /   a---a---b A     /           /
+               /   /               /           /
+              /   /   c---c---c---c B         /
+             /   /   /             \         /
+            /   /   /   b---b C     \       /
+           /   /   /   /             \     /
+    ---o---o---o---o---o---o---o---o---o---o---o "master"
+
+
+A, B and C are topic branches.
+
+ * A has one fix since it was merged up to "next".
+
+ * B has finished.  It has been fully merged up to "master" and "next",
+   and is ready to be deleted.
+
+ * C has not merged to "next" at all.
+
+We would want to allow C to be rebased, refuse A, and encourage
+B to be deleted.
+
+To compute (1):
+
+       git-rev-list ^master ^topic next
+       git-rev-list ^master        next
+
+       if these match, topic has not merged in next at all.
+
+To compute (2):
+
+       git-rev-list master..topic
+
+       if this is empty, it is fully merged to "master".