summaryrefslogtreecommitdiff
path: root/contrib
diff options
context:
space:
mode:
Diffstat (limited to 'contrib')
-rw-r--r--contrib/README44
-rw-r--r--contrib/blameview/README10
-rwxr-xr-xcontrib/blameview/blameview.perl155
-rwxr-xr-xcontrib/completion/git-completion.bash1036
-rw-r--r--contrib/continuous/cidaemon503
-rw-r--r--contrib/continuous/post-receive-cinotify104
-rw-r--r--contrib/emacs/.gitignore1
-rw-r--r--contrib/emacs/Makefile20
-rw-r--r--contrib/emacs/git-blame.el380
-rw-r--r--contrib/emacs/git.el1203
-rw-r--r--contrib/emacs/vc-git.el151
-rwxr-xr-xcontrib/examples/git-gc.sh37
-rwxr-xr-xcontrib/examples/git-resolve.sh112
-rwxr-xr-xcontrib/fast-import/import-tars.perl126
-rwxr-xr-xcontrib/gitview/gitview1029
-rw-r--r--contrib/gitview/gitview.txt56
-rwxr-xr-xcontrib/hg-to-git/hg-to-git.py233
-rw-r--r--contrib/hg-to-git/hg-to-git.txt21
-rw-r--r--contrib/hooks/post-receive-email588
-rw-r--r--contrib/remotes2config.sh35
-rw-r--r--contrib/vim/README8
-rw-r--r--contrib/vim/syntax/gitcommit.vim18
-rwxr-xr-xcontrib/workdir/git-new-workdir57
23 files changed, 5927 insertions, 0 deletions
diff --git a/contrib/README b/contrib/README
new file mode 100644
index 0000000000..e1c0a01ff3
--- /dev/null
+++ b/contrib/README
@@ -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/blameview/README b/contrib/blameview/README
new file mode 100644
index 0000000000..50a6f67fd6
--- /dev/null
+++ b/contrib/blameview/README
@@ -0,0 +1,10 @@
+This is a sample program to use 'git-blame --incremental', based
+on this message.
+
+From: Jeff King <peff@peff.net>
+Subject: Re: More precise tag following
+To: Linus Torvalds <torvalds@linux-foundation.org>
+Cc: git@vger.kernel.org
+Date: Sat, 27 Jan 2007 18:52:38 -0500
+Message-ID: <20070127235238.GA28706@coredump.intra.peff.net>
+
diff --git a/contrib/blameview/blameview.perl b/contrib/blameview/blameview.perl
new file mode 100755
index 0000000000..1dec00137b
--- /dev/null
+++ b/contrib/blameview/blameview.perl
@@ -0,0 +1,155 @@
+#!/usr/bin/perl
+
+use Gtk2 -init;
+use Gtk2::SimpleList;
+
+my $hash;
+my $fn;
+if ( @ARGV == 1 ) {
+ $hash = "HEAD";
+ $fn = shift;
+} elsif ( @ARGV == 2 ) {
+ $hash = shift;
+ $fn = shift;
+} else {
+ die "Usage blameview [<rev>] <filename>";
+}
+
+Gtk2::Rc->parse_string(<<'EOS');
+style "treeview_style"
+{
+ GtkTreeView::vertical-separator = 0
+}
+class "GtkTreeView" style "treeview_style"
+EOS
+
+my $window = Gtk2::Window->new('toplevel');
+$window->signal_connect(destroy => sub { Gtk2->main_quit });
+my $vpan = Gtk2::VPaned->new();
+$window->add($vpan);
+my $scrolled_window = Gtk2::ScrolledWindow->new;
+$vpan->pack1($scrolled_window, 1, 1);
+my $fileview = Gtk2::SimpleList->new(
+ 'Commit' => 'text',
+ 'FileLine' => 'text',
+ 'Data' => 'text'
+);
+$scrolled_window->add($fileview);
+$fileview->get_column(0)->set_spacing(0);
+$fileview->set_size_request(1024, 768);
+$fileview->set_rules_hint(1);
+$fileview->signal_connect (row_activated => sub {
+ my ($sl, $path, $column) = @_;
+ my $row_ref = $sl->get_row_data_from_path ($path);
+ system("blameview @$row_ref[0]~1 $fn &");
+ });
+
+my $commitwindow = Gtk2::ScrolledWindow->new();
+$commitwindow->set_policy ('GTK_POLICY_AUTOMATIC','GTK_POLICY_AUTOMATIC');
+$vpan->pack2($commitwindow, 1, 1);
+my $commit_text = Gtk2::TextView->new();
+my $commit_buffer = Gtk2::TextBuffer->new();
+$commit_text->set_buffer($commit_buffer);
+$commitwindow->add($commit_text);
+
+$fileview->signal_connect (cursor_changed => sub {
+ my ($sl) = @_;
+ my ($path, $focus_column) = $sl->get_cursor();
+ my $row_ref = $sl->get_row_data_from_path ($path);
+ my $c_fh;
+ open($c_fh, '-|', "git cat-file commit @$row_ref[0]")
+ or die "unable to find commit @$row_ref[0]";
+ my @buffer = <$c_fh>;
+ $commit_buffer->set_text("@buffer");
+ close($c_fh);
+ });
+
+my $fh;
+open($fh, '-|', "git cat-file blob $hash:$fn")
+ or die "unable to open $fn: $!";
+
+while(<$fh>) {
+ chomp;
+ $fileview->{data}->[$.] = ['HEAD', "$fn:$.", $_];
+}
+
+my $blame;
+open($blame, '-|', qw(git blame --incremental --), $fn, $hash)
+ or die "cannot start git-blame $fn";
+
+Glib::IO->add_watch(fileno($blame), 'in', \&read_blame_line);
+
+$window->show_all;
+Gtk2->main;
+exit 0;
+
+my %commitinfo = ();
+
+sub flush_blame_line {
+ my ($attr) = @_;
+
+ return unless defined $attr;
+
+ my ($commit, $s_lno, $lno, $cnt) =
+ @{$attr}{qw(COMMIT S_LNO LNO CNT)};
+
+ my ($filename, $author, $author_time, $author_tz) =
+ @{$commitinfo{$commit}}{qw(FILENAME AUTHOR AUTHOR-TIME AUTHOR-TZ)};
+ my $info = $author . ' ' . format_time($author_time, $author_tz);
+
+ for(my $i = 0; $i < $cnt; $i++) {
+ @{$fileview->{data}->[$lno+$i-1]}[0,1,2] =
+ (substr($commit, 0, 8), $filename . ':' . ($s_lno+$i));
+ }
+}
+
+my $buf;
+my $current;
+sub read_blame_line {
+
+ my $r = sysread($blame, $buf, 1024, length($buf));
+ die "I/O error" unless defined $r;
+
+ if ($r == 0) {
+ flush_blame_line($current);
+ $current = undef;
+ return 0;
+ }
+
+ while ($buf =~ s/([^\n]*)\n//) {
+ my $line = $1;
+
+ if (($commit, $s_lno, $lno, $cnt) =
+ ($line =~ /^([0-9a-f]{40}) (\d+) (\d+) (\d+)$/)) {
+ flush_blame_line($current);
+ $current = +{
+ COMMIT => $1,
+ S_LNO => $2,
+ LNO => $3,
+ CNT => $4,
+ };
+ next;
+ }
+
+ # extended attribute values
+ if ($line =~ /^(author|author-mail|author-time|author-tz|committer|committer-mail|committer-time|committer-tz|summary|filename) (.*)$/) {
+ my $commit = $current->{COMMIT};
+ $commitinfo{$commit}{uc($1)} = $2;
+ next;
+ }
+ }
+ return 1;
+}
+
+sub format_time {
+ my $time = shift;
+ my $tz = shift;
+
+ my $minutes = $tz < 0 ? 0-$tz : $tz;
+ $minutes = ($minutes / 100)*60 + ($minutes % 100);
+ $minutes = $tz < 0 ? 0-$minutes : $minutes;
+ $time += $minutes * 60;
+ my @t = gmtime($time);
+ return sprintf('%04d-%02d-%02d %02d:%02d:%02d %s',
+ $t[5] + 1900, @t[4,3,2,1,0], $tz);
+}
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
new file mode 100755
index 0000000000..7c03403484
--- /dev/null
+++ b/contrib/completion/git-completion.bash
@@ -0,0 +1,1036 @@
+#
+# bash completion support for core Git.
+#
+# Copyright (C) 2006,2007 Shawn Pearce
+# Conceptually based on gitcompletion (http://gitweb.hawaga.org.uk/).
+#
+# The contained completion routines provide support for completing:
+#
+# *) local and remote branch names
+# *) local and remote tag names
+# *) .git/remotes file names
+# *) git 'subcommands'
+# *) tree paths within 'ref:path/to/file' expressions
+#
+# To use these routines:
+#
+# 1) Copy this file to somewhere (e.g. ~/.git-completion.sh).
+# 2) Added the following line to your .bashrc:
+# source ~/.git-completion.sh
+#
+# 3) You may want to make sure the git executable is available
+# in your PATH before this script is sourced, as some caching
+# is performed while the script loads. If git isn't found
+# at source time then all lookups will be done on demand,
+# which may be slightly slower.
+#
+# 4) Consider changing your PS1 to also show the current branch:
+# PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ '
+#
+# The argument to __git_ps1 will be displayed only if you
+# are currently in a git repository. The %s token will be
+# the name of the current branch.
+#
+
+__gitdir ()
+{
+ if [ -z "$1" ]; then
+ if [ -n "$__git_dir" ]; then
+ echo "$__git_dir"
+ elif [ -d .git ]; then
+ echo .git
+ else
+ git rev-parse --git-dir 2>/dev/null
+ fi
+ elif [ -d "$1/.git" ]; then
+ echo "$1/.git"
+ else
+ echo "$1"
+ fi
+}
+
+__git_ps1 ()
+{
+ local b="$(git symbolic-ref HEAD 2>/dev/null)"
+ if [ -n "$b" ]; then
+ if [ -n "$1" ]; then
+ printf "$1" "${b##refs/heads/}"
+ else
+ printf " (%s)" "${b##refs/heads/}"
+ fi
+ fi
+}
+
+__gitcomp ()
+{
+ local all c s=$'\n' IFS=' '$'\t'$'\n'
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ if [ $# -gt 2 ]; then
+ cur="$3"
+ fi
+ for c in $1; do
+ case "$c$4" in
+ --*=*) all="$all$c$4$s" ;;
+ *.) all="$all$c$4$s" ;;
+ *) all="$all$c$4 $s" ;;
+ esac
+ done
+ IFS=$s
+ COMPREPLY=($(compgen -P "$2" -W "$all" -- "$cur"))
+ return
+}
+
+__git_heads ()
+{
+ local cmd i is_hash=y dir="$(__gitdir "$1")"
+ if [ -d "$dir" ]; then
+ for i in $(git --git-dir="$dir" \
+ for-each-ref --format='%(refname)' \
+ refs/heads ); do
+ echo "${i#refs/heads/}"
+ done
+ return
+ fi
+ for i in $(git-ls-remote "$1" 2>/dev/null); do
+ case "$is_hash,$i" in
+ y,*) is_hash=n ;;
+ n,*^{}) is_hash=y ;;
+ n,refs/heads/*) is_hash=y; echo "${i#refs/heads/}" ;;
+ n,*) is_hash=y; echo "$i" ;;
+ esac
+ done
+}
+
+__git_refs ()
+{
+ local cmd i is_hash=y dir="$(__gitdir "$1")"
+ if [ -d "$dir" ]; then
+ if [ -e "$dir/HEAD" ]; then echo HEAD; fi
+ for i in $(git --git-dir="$dir" \
+ for-each-ref --format='%(refname)' \
+ refs/tags refs/heads refs/remotes); do
+ case "$i" in
+ refs/tags/*) echo "${i#refs/tags/}" ;;
+ refs/heads/*) echo "${i#refs/heads/}" ;;
+ refs/remotes/*) echo "${i#refs/remotes/}" ;;
+ *) echo "$i" ;;
+ esac
+ done
+ return
+ fi
+ for i in $(git-ls-remote "$dir" 2>/dev/null); do
+ case "$is_hash,$i" in
+ y,*) is_hash=n ;;
+ n,*^{}) is_hash=y ;;
+ n,refs/tags/*) is_hash=y; echo "${i#refs/tags/}" ;;
+ n,refs/heads/*) is_hash=y; echo "${i#refs/heads/}" ;;
+ n,refs/remotes/*) is_hash=y; echo "${i#refs/remotes/}" ;;
+ n,*) is_hash=y; echo "$i" ;;
+ esac
+ done
+}
+
+__git_refs2 ()
+{
+ local i
+ for i in $(__git_refs "$1"); do
+ echo "$i:$i"
+ done
+}
+
+__git_refs_remotes ()
+{
+ local cmd i is_hash=y
+ for i in $(git-ls-remote "$1" 2>/dev/null); do
+ case "$is_hash,$i" in
+ n,refs/heads/*)
+ is_hash=y
+ echo "$i:refs/remotes/$1/${i#refs/heads/}"
+ ;;
+ y,*) is_hash=n ;;
+ n,*^{}) is_hash=y ;;
+ n,refs/tags/*) is_hash=y;;
+ n,*) is_hash=y; ;;
+ esac
+ done
+}
+
+__git_remotes ()
+{
+ local i ngoff IFS=$'\n' d="$(__gitdir)"
+ shopt -q nullglob || ngoff=1
+ shopt -s nullglob
+ for i in "$d/remotes"/*; do
+ echo ${i#$d/remotes/}
+ done
+ [ "$ngoff" ] && shopt -u nullglob
+ for i in $(git --git-dir="$d" config --list); do
+ case "$i" in
+ remote.*.url=*)
+ i="${i#remote.}"
+ echo "${i/.url=*/}"
+ ;;
+ esac
+ done
+}
+
+__git_merge_strategies ()
+{
+ if [ -n "$__git_merge_strategylist" ]; then
+ echo "$__git_merge_strategylist"
+ return
+ fi
+ sed -n "/^all_strategies='/{
+ s/^all_strategies='//
+ s/'//
+ p
+ q
+ }" "$(git --exec-path)/git-merge"
+}
+__git_merge_strategylist=
+__git_merge_strategylist="$(__git_merge_strategies 2>/dev/null)"
+
+__git_complete_file ()
+{
+ local pfx ls ref cur="${COMP_WORDS[COMP_CWORD]}"
+ case "$cur" in
+ ?*:*)
+ ref="${cur%%:*}"
+ cur="${cur#*:}"
+ case "$cur" in
+ ?*/*)
+ pfx="${cur%/*}"
+ cur="${cur##*/}"
+ ls="$ref:$pfx"
+ pfx="$pfx/"
+ ;;
+ *)
+ ls="$ref"
+ ;;
+ esac
+ COMPREPLY=($(compgen -P "$pfx" \
+ -W "$(git --git-dir="$(__gitdir)" ls-tree "$ls" \
+ | sed '/^100... blob /s,^.* ,,
+ /^040000 tree /{
+ s,^.* ,,
+ s,$,/,
+ }
+ s/^.* //')" \
+ -- "$cur"))
+ ;;
+ *)
+ __gitcomp "$(__git_refs)"
+ ;;
+ esac
+}
+
+__git_complete_revlist ()
+{
+ local pfx cur="${COMP_WORDS[COMP_CWORD]}"
+ case "$cur" in
+ *...*)
+ pfx="${cur%...*}..."
+ cur="${cur#*...}"
+ __gitcomp "$(__git_refs)" "$pfx" "$cur"
+ ;;
+ *..*)
+ pfx="${cur%..*}.."
+ cur="${cur#*..}"
+ __gitcomp "$(__git_refs)" "$pfx" "$cur"
+ ;;
+ *.)
+ __gitcomp "$cur."
+ ;;
+ *)
+ __gitcomp "$(__git_refs)"
+ ;;
+ esac
+}
+
+__git_commands ()
+{
+ if [ -n "$__git_commandlist" ]; then
+ echo "$__git_commandlist"
+ return
+ fi
+ local i IFS=" "$'\n'
+ for i in $(git help -a|egrep '^ ')
+ do
+ case $i in
+ add--interactive) : plumbing;;
+ applymbox) : ask gittus;;
+ applypatch) : ask gittus;;
+ archimport) : import;;
+ cat-file) : plumbing;;
+ check-ref-format) : plumbing;;
+ commit-tree) : plumbing;;
+ convert-objects) : plumbing;;
+ cvsexportcommit) : export;;
+ cvsimport) : import;;
+ cvsserver) : daemon;;
+ daemon) : daemon;;
+ fast-import) : import;;
+ fsck-objects) : plumbing;;
+ fetch-pack) : plumbing;;
+ fmt-merge-msg) : plumbing;;
+ hash-object) : plumbing;;
+ http-*) : transport;;
+ index-pack) : plumbing;;
+ init-db) : deprecated;;
+ local-fetch) : plumbing;;
+ mailinfo) : plumbing;;
+ mailsplit) : plumbing;;
+ merge-*) : plumbing;;
+ mktree) : plumbing;;
+ mktag) : plumbing;;
+ pack-objects) : plumbing;;
+ pack-redundant) : plumbing;;
+ pack-refs) : plumbing;;
+ parse-remote) : plumbing;;
+ patch-id) : plumbing;;
+ peek-remote) : plumbing;;
+ prune) : plumbing;;
+ prune-packed) : plumbing;;
+ quiltimport) : import;;
+ read-tree) : plumbing;;
+ receive-pack) : plumbing;;
+ reflog) : plumbing;;
+ repo-config) : plumbing;;
+ rerere) : plumbing;;
+ rev-list) : plumbing;;
+ rev-parse) : plumbing;;
+ runstatus) : plumbing;;
+ sh-setup) : internal;;
+ shell) : daemon;;
+ send-pack) : plumbing;;
+ show-index) : plumbing;;
+ ssh-*) : transport;;
+ stripspace) : plumbing;;
+ svn) : import export;;
+ svnimport) : import;;
+ symbolic-ref) : plumbing;;
+ tar-tree) : deprecated;;
+ unpack-file) : plumbing;;
+ unpack-objects) : plumbing;;
+ update-index) : plumbing;;
+ update-ref) : plumbing;;
+ update-server-info) : daemon;;
+ upload-archive) : plumbing;;
+ upload-pack) : plumbing;;
+ write-tree) : plumbing;;
+ verify-tag) : plumbing;;
+ *) echo $i;;
+ esac
+ done
+}
+__git_commandlist=
+__git_commandlist="$(__git_commands 2>/dev/null)"
+
+__git_aliases ()
+{
+ local i IFS=$'\n'
+ for i in $(git --git-dir="$(__gitdir)" config --list); do
+ case "$i" in
+ alias.*)
+ i="${i#alias.}"
+ echo "${i/=*/}"
+ ;;
+ esac
+ done
+}
+
+__git_aliased_command ()
+{
+ local word cmdline=$(git --git-dir="$(__gitdir)" \
+ config --get "alias.$1")
+ for word in $cmdline; do
+ if [ "${word##-*}" ]; then
+ echo $word
+ return
+ fi
+ done
+}
+
+__git_whitespacelist="nowarn warn error error-all strip"
+
+_git_am ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ if [ -d .dotest ]; then
+ __gitcomp "--skip --resolved"
+ return
+ fi
+ case "$cur" in
+ --whitespace=*)
+ __gitcomp "$__git_whitespacelist" "" "${cur##--whitespace=}"
+ return
+ ;;
+ --*)
+ __gitcomp "
+ --signoff --utf8 --binary --3way --interactive
+ --whitespace=
+ "
+ return
+ esac
+ COMPREPLY=()
+}
+
+_git_apply ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ case "$cur" in
+ --whitespace=*)
+ __gitcomp "$__git_whitespacelist" "" "${cur##--whitespace=}"
+ return
+ ;;
+ --*)
+ __gitcomp "
+ --stat --numstat --summary --check --index
+ --cached --index-info --reverse --reject --unidiff-zero
+ --apply --no-add --exclude=
+ --whitespace= --inaccurate-eof --verbose
+ "
+ return
+ esac
+ COMPREPLY=()
+}
+
+_git_add ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ case "$cur" in
+ --*)
+ __gitcomp "--interactive"
+ return
+ esac
+ COMPREPLY=()
+}
+
+_git_bisect ()
+{
+ local i c=1 command
+ while [ $c -lt $COMP_CWORD ]; do
+ i="${COMP_WORDS[c]}"
+ case "$i" in
+ start|bad|good|reset|visualize|replay|log)
+ command="$i"
+ break
+ ;;
+ esac
+ c=$((++c))
+ done
+
+ if [ $c -eq $COMP_CWORD -a -z "$command" ]; then
+ __gitcomp "start bad good reset visualize replay log"
+ return
+ fi
+
+ case "$command" in
+ bad|good|reset)
+ __gitcomp "$(__git_refs)"
+ ;;
+ *)
+ COMPREPLY=()
+ ;;
+ esac
+}
+
+_git_branch ()
+{
+ __gitcomp "$(__git_refs)"
+}
+
+_git_checkout ()
+{
+ __gitcomp "$(__git_refs)"
+}
+
+_git_cherry ()
+{
+ __gitcomp "$(__git_refs)"
+}
+
+_git_cherry_pick ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ case "$cur" in
+ --*)
+ __gitcomp "--edit --no-commit"
+ ;;
+ *)
+ __gitcomp "$(__git_refs)"
+ ;;
+ esac
+}
+
+_git_commit ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ case "$cur" in
+ --*)
+ __gitcomp "
+ --all --author= --signoff --verify --no-verify
+ --edit --amend --include --only
+ "
+ return
+ esac
+ COMPREPLY=()
+}
+
+_git_diff ()
+{
+ __git_complete_file
+}
+
+_git_diff_tree ()
+{
+ __gitcomp "$(__git_refs)"
+}
+
+_git_fetch ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+
+ case "${COMP_WORDS[0]},$COMP_CWORD" in
+ git-fetch*,1)
+ __gitcomp "$(__git_remotes)"
+ ;;
+ git,2)
+ __gitcomp "$(__git_remotes)"
+ ;;
+ *)
+ case "$cur" in
+ *:*)
+ __gitcomp "$(__git_refs)" "" "${cur#*:}"
+ ;;
+ *)
+ local remote
+ case "${COMP_WORDS[0]}" in
+ git-fetch) remote="${COMP_WORDS[1]}" ;;
+ git) remote="${COMP_WORDS[2]}" ;;
+ esac
+ __gitcomp "$(__git_refs2 "$remote")"
+ ;;
+ esac
+ ;;
+ esac
+}
+
+_git_format_patch ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ case "$cur" in
+ --*)
+ __gitcomp "
+ --stdout --attach --thread
+ --output-directory
+ --numbered --start-number
+ --keep-subject
+ --signoff
+ --in-reply-to=
+ --full-index --binary
+ --not --all
+ "
+ return
+ ;;
+ esac
+ __git_complete_revlist
+}
+
+_git_gc ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ case "$cur" in
+ --*)
+ __gitcomp "--prune"
+ return
+ ;;
+ esac
+ COMPREPLY=()
+}
+
+_git_ls_remote ()
+{
+ __gitcomp "$(__git_remotes)"
+}
+
+_git_ls_tree ()
+{
+ __git_complete_file
+}
+
+_git_log ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ case "$cur" in
+ --pretty=*)
+ __gitcomp "
+ oneline short medium full fuller email raw
+ " "" "${cur##--pretty=}"
+ return
+ ;;
+ --*)
+ __gitcomp "
+ --max-count= --max-age= --since= --after=
+ --min-age= --before= --until=
+ --root --not --topo-order --date-order
+ --no-merges
+ --abbrev-commit --abbrev=
+ --relative-date
+ --author= --committer= --grep=
+ --all-match
+ --pretty= --name-status --name-only
+ --not --all
+ "
+ return
+ ;;
+ esac
+ __git_complete_revlist
+}
+
+_git_merge ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ case "${COMP_WORDS[COMP_CWORD-1]}" in
+ -s|--strategy)
+ __gitcomp "$(__git_merge_strategies)"
+ return
+ esac
+ case "$cur" in
+ --strategy=*)
+ __gitcomp "$(__git_merge_strategies)" "" "${cur##--strategy=}"
+ return
+ ;;
+ --*)
+ __gitcomp "
+ --no-commit --no-summary --squash --strategy
+ "
+ return
+ esac
+ __gitcomp "$(__git_refs)"
+}
+
+_git_merge_base ()
+{
+ __gitcomp "$(__git_refs)"
+}
+
+_git_name_rev ()
+{
+ __gitcomp "--tags --all --stdin"
+}
+
+_git_pull ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+
+ case "${COMP_WORDS[0]},$COMP_CWORD" in
+ git-pull*,1)
+ __gitcomp "$(__git_remotes)"
+ ;;
+ git,2)
+ __gitcomp "$(__git_remotes)"
+ ;;
+ *)
+ local remote
+ case "${COMP_WORDS[0]}" in
+ git-pull) remote="${COMP_WORDS[1]}" ;;
+ git) remote="${COMP_WORDS[2]}" ;;
+ esac
+ __gitcomp "$(__git_refs "$remote")"
+ ;;
+ esac
+}
+
+_git_push ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+
+ case "${COMP_WORDS[0]},$COMP_CWORD" in
+ git-push*,1)
+ __gitcomp "$(__git_remotes)"
+ ;;
+ git,2)
+ __gitcomp "$(__git_remotes)"
+ ;;
+ *)
+ case "$cur" in
+ *:*)
+ local remote
+ case "${COMP_WORDS[0]}" in
+ git-push) remote="${COMP_WORDS[1]}" ;;
+ git) remote="${COMP_WORDS[2]}" ;;
+ esac
+ __gitcomp "$(__git_refs "$remote")" "" "${cur#*:}"
+ ;;
+ *)
+ __gitcomp "$(__git_refs2)"
+ ;;
+ esac
+ ;;
+ esac
+}
+
+_git_rebase ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ if [ -d .dotest ] || [ -d .git/.dotest-merge ]; then
+ __gitcomp "--continue --skip --abort"
+ return
+ fi
+ case "${COMP_WORDS[COMP_CWORD-1]}" in
+ -s|--strategy)
+ __gitcomp "$(__git_merge_strategies)"
+ return
+ esac
+ case "$cur" in
+ --strategy=*)
+ __gitcomp "$(__git_merge_strategies)" "" "${cur##--strategy=}"
+ return
+ ;;
+ --*)
+ __gitcomp "--onto --merge --strategy"
+ return
+ esac
+ __gitcomp "$(__git_refs)"
+}
+
+_git_config ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ local prv="${COMP_WORDS[COMP_CWORD-1]}"
+ case "$prv" in
+ branch.*.remote)
+ __gitcomp "$(__git_remotes)"
+ return
+ ;;
+ branch.*.merge)
+ __gitcomp "$(__git_refs)"
+ return
+ ;;
+ remote.*.fetch)
+ local remote="${prv#remote.}"
+ remote="${remote%.fetch}"
+ __gitcomp "$(__git_refs_remotes "$remote")"
+ return
+ ;;
+ remote.*.push)
+ local remote="${prv#remote.}"
+ remote="${remote%.push}"
+ __gitcomp "$(git --git-dir="$(__gitdir)" \
+ for-each-ref --format='%(refname):%(refname)' \
+ refs/heads)"
+ return
+ ;;
+ pull.twohead|pull.octopus)
+ __gitcomp "$(__git_merge_strategies)"
+ return
+ ;;
+ color.branch|color.diff|color.status)
+ __gitcomp "always never auto"
+ return
+ ;;
+ color.*.*)
+ __gitcomp "
+ black red green yellow blue magenta cyan white
+ bold dim ul blink reverse
+ "
+ return
+ ;;
+ *.*)
+ COMPREPLY=()
+ return
+ ;;
+ esac
+ case "$cur" in
+ --*)
+ __gitcomp "
+ --global --list --replace-all
+ --get --get-all --get-regexp
+ --add --unset --unset-all
+ "
+ return
+ ;;
+ branch.*.*)
+ local pfx="${cur%.*}."
+ cur="${cur##*.}"
+ __gitcomp "remote merge" "$pfx" "$cur"
+ return
+ ;;
+ branch.*)
+ local pfx="${cur%.*}."
+ cur="${cur#*.}"
+ __gitcomp "$(__git_heads)" "$pfx" "$cur" "."
+ return
+ ;;
+ remote.*.*)
+ local pfx="${cur%.*}."
+ cur="${cur##*.}"
+ __gitcomp "url fetch push" "$pfx" "$cur"
+ return
+ ;;
+ remote.*)
+ local pfx="${cur%.*}."
+ cur="${cur#*.}"
+ __gitcomp "$(__git_remotes)" "$pfx" "$cur" "."
+ return
+ ;;
+ esac
+ __gitcomp "
+ apply.whitespace
+ core.fileMode
+ core.gitProxy
+ core.ignoreStat
+ core.preferSymlinkRefs
+ core.logAllRefUpdates
+ core.repositoryFormatVersion
+ core.sharedRepository
+ core.warnAmbiguousRefs
+ core.compression
+ core.legacyHeaders
+ core.packedGitWindowSize
+ core.packedGitLimit
+ color.branch
+ color.branch.current
+ color.branch.local
+ color.branch.remote
+ color.branch.plain
+ color.diff
+ color.diff.plain
+ color.diff.meta
+ color.diff.frag
+ color.diff.old
+ color.diff.new
+ color.diff.commit
+ color.diff.whitespace
+ color.pager
+ color.status
+ color.status.header
+ color.status.added
+ color.status.changed
+ color.status.untracked
+ diff.renameLimit
+ diff.renames
+ fetch.unpackLimit
+ format.headers
+ gitcvs.enabled
+ gitcvs.logfile
+ gc.reflogexpire
+ gc.reflogexpireunreachable
+ gc.rerereresolved
+ gc.rerereunresolved
+ http.sslVerify
+ http.sslCert
+ http.sslKey
+ http.sslCAInfo
+ http.sslCAPath
+ http.maxRequests
+ http.lowSpeedLimit
+ http.lowSpeedTime
+ http.noEPSV
+ i18n.commitEncoding
+ i18n.logOutputEncoding
+ log.showroot
+ merge.summary
+ merge.verbosity
+ pack.window
+ pull.octopus
+ pull.twohead
+ repack.useDeltaBaseOffset
+ show.difftree
+ showbranch.default
+ tar.umask
+ transfer.unpackLimit
+ receive.unpackLimit
+ receive.denyNonFastForwards
+ user.name
+ user.email
+ user.signingkey
+ whatchanged.difftree
+ branch. remote.
+ "
+}
+
+_git_remote ()
+{
+ local i c=1 command
+ while [ $c -lt $COMP_CWORD ]; do
+ i="${COMP_WORDS[c]}"
+ case "$i" in
+ add|show|prune) command="$i"; break ;;
+ esac
+ c=$((++c))
+ done
+
+ if [ $c -eq $COMP_CWORD -a -z "$command" ]; then
+ __gitcomp "add show prune"
+ return
+ fi
+
+ case "$command" in
+ show|prune)
+ __gitcomp "$(__git_remotes)"
+ ;;
+ *)
+ COMPREPLY=()
+ ;;
+ esac
+}
+
+_git_reset ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ case "$cur" in
+ --*)
+ __gitcomp "--mixed --hard --soft"
+ return
+ ;;
+ esac
+ __gitcomp "$(__git_refs)"
+}
+
+_git_show ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ case "$cur" in
+ --pretty=*)
+ __gitcomp "
+ oneline short medium full fuller email raw
+ " "" "${cur##--pretty=}"
+ return
+ ;;
+ --*)
+ __gitcomp "--pretty="
+ return
+ ;;
+ esac
+ __git_complete_file
+}
+
+_git ()
+{
+ local i c=1 command __git_dir
+
+ while [ $c -lt $COMP_CWORD ]; do
+ i="${COMP_WORDS[c]}"
+ case "$i" in
+ --git-dir=*) __git_dir="${i#--git-dir=}" ;;
+ --bare) __git_dir="." ;;
+ --version|--help|-p|--paginate) ;;
+ *) command="$i"; break ;;
+ esac
+ c=$((++c))
+ done
+
+ if [ $c -eq $COMP_CWORD -a -z "$command" ]; then
+ case "${COMP_WORDS[COMP_CWORD]}" in
+ --*=*) COMPREPLY=() ;;
+ --*) __gitcomp "--git-dir= --bare --version --exec-path" ;;
+ *) __gitcomp "$(__git_commands) $(__git_aliases)" ;;
+ esac
+ return
+ fi
+
+ local expansion=$(__git_aliased_command "$command")
+ [ "$expansion" ] && command="$expansion"
+
+ case "$command" in
+ am) _git_am ;;
+ add) _git_add ;;
+ apply) _git_apply ;;
+ bisect) _git_bisect ;;
+ branch) _git_branch ;;
+ checkout) _git_checkout ;;
+ cherry) _git_cherry ;;
+ cherry-pick) _git_cherry_pick ;;
+ commit) _git_commit ;;
+ config) _git_config ;;
+ diff) _git_diff ;;
+ diff-tree) _git_diff_tree ;;
+ fetch) _git_fetch ;;
+ format-patch) _git_format_patch ;;
+ gc) _git_gc ;;
+ log) _git_log ;;
+ ls-remote) _git_ls_remote ;;
+ ls-tree) _git_ls_tree ;;
+ merge) _git_merge;;
+ merge-base) _git_merge_base ;;
+ name-rev) _git_name_rev ;;
+ pull) _git_pull ;;
+ push) _git_push ;;
+ rebase) _git_rebase ;;
+ remote) _git_remote ;;
+ reset) _git_reset ;;
+ show) _git_show ;;
+ show-branch) _git_log ;;
+ whatchanged) _git_log ;;
+ *) COMPREPLY=() ;;
+ esac
+}
+
+_gitk ()
+{
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ case "$cur" in
+ --*)
+ __gitcomp "--not --all"
+ return
+ ;;
+ esac
+ __git_complete_revlist
+}
+
+complete -o default -o nospace -F _git git
+complete -o default -o nospace -F _gitk gitk
+complete -o default -o nospace -F _git_am git-am
+complete -o default -o nospace -F _git_apply git-apply
+complete -o default -o nospace -F _git_bisect git-bisect
+complete -o default -o nospace -F _git_branch git-branch
+complete -o default -o nospace -F _git_checkout git-checkout
+complete -o default -o nospace -F _git_cherry git-cherry
+complete -o default -o nospace -F _git_cherry_pick git-cherry-pick
+complete -o default -o nospace -F _git_commit git-commit
+complete -o default -o nospace -F _git_diff git-diff
+complete -o default -o nospace -F _git_diff_tree git-diff-tree
+complete -o default -o nospace -F _git_fetch git-fetch
+complete -o default -o nospace -F _git_format_patch git-format-patch
+complete -o default -o nospace -F _git_gc git-gc
+complete -o default -o nospace -F _git_log git-log
+complete -o default -o nospace -F _git_ls_remote git-ls-remote
+complete -o default -o nospace -F _git_ls_tree git-ls-tree
+complete -o default -o nospace -F _git_merge git-merge
+complete -o default -o nospace -F _git_merge_base git-merge-base
+complete -o default -o nospace -F _git_name_rev git-name-rev
+complete -o default -o nospace -F _git_pull git-pull
+complete -o default -o nospace -F _git_push git-push
+complete -o default -o nospace -F _git_rebase git-rebase
+complete -o default -o nospace -F _git_config git-config
+complete -o default -o nospace -F _git_remote git-remote
+complete -o default -o nospace -F _git_reset git-reset
+complete -o default -o nospace -F _git_show git-show
+complete -o default -o nospace -F _git_log git-show-branch
+complete -o default -o nospace -F _git_log git-whatchanged
+
+# The following are necessary only for Cygwin, and only are needed
+# when the user has tab-completed the executable name and consequently
+# included the '.exe' suffix.
+#
+if [ Cygwin = "$(uname -o 2>/dev/null)" ]; then
+complete -o default -o nospace -F _git_add git-add.exe
+complete -o default -o nospace -F _git_apply git-apply.exe
+complete -o default -o nospace -F _git git.exe
+complete -o default -o nospace -F _git_branch git-branch.exe
+complete -o default -o nospace -F _git_cherry git-cherry.exe
+complete -o default -o nospace -F _git_diff git-diff.exe
+complete -o default -o nospace -F _git_diff_tree git-diff-tree.exe
+complete -o default -o nospace -F _git_format_patch git-format-patch.exe
+complete -o default -o nospace -F _git_log git-log.exe
+complete -o default -o nospace -F _git_ls_tree git-ls-tree.exe
+complete -o default -o nospace -F _git_merge_base git-merge-base.exe
+complete -o default -o nospace -F _git_name_rev git-name-rev.exe
+complete -o default -o nospace -F _git_push git-push.exe
+complete -o default -o nospace -F _git_config git-config
+complete -o default -o nospace -F _git_show git-show.exe
+complete -o default -o nospace -F _git_log git-show-branch.exe
+complete -o default -o nospace -F _git_log git-whatchanged.exe
+fi
diff --git a/contrib/continuous/cidaemon b/contrib/continuous/cidaemon
new file mode 100644
index 0000000000..4009a151de
--- /dev/null
+++ b/contrib/continuous/cidaemon
@@ -0,0 +1,503 @@
+#!/usr/bin/perl
+#
+# A daemon that waits for update events sent by its companion
+# post-receive-cinotify hook, checks out a new copy of source,
+# compiles it, and emails the guilty parties if the compile
+# (and optionally test suite) fails.
+#
+# To use this daemon, configure it and run it. It will disconnect
+# from your terminal and fork into the background. The daemon must
+# have local filesystem access to the source repositories, as it
+# uses objects/info/alternates to avoid copying objects.
+#
+# Add its companion post-receive-cinotify hook as the post-receive
+# hook to each repository that the daemon should monitor. Yes, a
+# single daemon can monitor more than one repository.
+#
+# To use multiple daemons on the same system, give them each a
+# unique queue file and tmpdir.
+#
+# Global Config
+# -------------
+# Reads from a Git style configuration file. This will be
+# ~/.gitconfig by default but can be overridden by setting
+# the GIT_CONFIG_FILE environment variable before starting.
+#
+# cidaemon.smtpHost
+# Hostname of the SMTP server the daemon will send email
+# through. Defaults to 'localhost'.
+#
+# cidaemon.smtpUser
+# Username to authenticate to the SMTP server as. This
+# variable is optional; if it is not supplied then no
+# authentication will be performed.
+#
+# cidaemon.smtpPassword
+# Password to authenticate to the SMTP server as. This
+# variable is optional. If not supplied but smtpUser was,
+# the daemon prompts for the password before forking into
+# the background.
+#
+# cidaemon.smtpAuth
+# Type of authentication to perform with the SMTP server.
+# If set to 'login' and smtpUser was defined, this will
+# use the AUTH LOGIN command, which is suitable for use
+# with at least one version of Microsoft Exchange Server.
+# If not set the daemon will use whatever auth methods
+# are supported by your version of Net::SMTP.
+#
+# cidaemon.email
+# Email address that daemon generated emails will be sent
+# from. This should be a useful email address within your
+# organization. Required.
+#
+# cidaemon.name
+# Human friendly name that the daemon will send emails as.
+# Defaults to 'cidaemon'.
+#
+# cidaemon.scanDelay
+# Number of seconds to sleep between polls of the queue file.
+# Defaults to 60.
+#
+# cidaemon.recentCache
+# Number of recent commit SHA-1s per repository to cache and
+# skip building if they appear again. This is useful to avoid
+# rebuilding the same commit multiple times just because it was
+# pushed into more than one branch. Defaults to 100.
+#
+# cidaemon.tmpdir
+# Scratch directory to create the builds within. The daemon
+# makes a new subdirectory for each build, then deletes it when
+# the build has finished. The pid file is also placed here.
+# Defaults to '/tmp'.
+#
+# cidaemon.queue
+# Path to the queue file that the post-receive-cinotify hook
+# appends events to. This file is polled by the daemon. It
+# must not be on an NFS mount (uses flock). Required.
+#
+# cidaemon.nocc
+# Perl regex patterns to match against author and committer
+# lines. If a pattern matches, that author or committer will
+# not be notified of a build failure.
+#
+# Per Repository Config
+# ----------------------
+# Read from the source repository's config file.
+#
+# builder.command
+# Shell command to execute the build. This command must
+# return 0 on "success" and non-zero on failure. If you
+# also want to run a test suite, make sure your command
+# does that too. Required.
+#
+# builder.queue
+# Queue file to notify the cidaemon through. Should match
+# cidaemon.queue. If not set the hook will not notify the
+# cidaemon.
+#
+# builder.skip
+# Perl regex patterns of refs that should not be sent to
+# cidaemon. Updates of these refs will be ignored.
+#
+# builder.newBranchBase
+# Glob patterns of refs that should be used to form the
+# 'old' revions of a newly created ref. This should set
+# to be globs that match your 'mainline' branches. This
+# way a build failure of a brand new topic branch does not
+# attempt to email everyone since the beginning of time;
+# instead it only emails those authors of commits not in
+# these 'mainline' branches.
+
+local $ENV{PATH} = join ':', qw(
+ /opt/git/bin
+ /usr/bin
+ /bin
+ );
+
+use strict;
+use warnings;
+use FindBin qw($RealBin);
+use File::Spec;
+use lib File::Spec->catfile($RealBin, '..', 'perl5');
+use Storable qw(retrieve nstore);
+use Fcntl ':flock';
+use POSIX qw(strftime);
+use Getopt::Long qw(:config no_auto_abbrev auto_help);
+
+sub git_config ($;$)
+{
+ my $var = shift;
+ my $required = shift || 0;
+ local *GIT;
+ open GIT, '-|','git','config','--get',$var;
+ my $r = <GIT>;
+ chop $r if $r;
+ close GIT;
+ die "error: $var not set.\n" if ($required && !$r);
+ return $r;
+}
+
+package EXCHANGE_NET_SMTP;
+
+# Microsoft Exchange Server requires an 'AUTH LOGIN'
+# style of authentication. This is different from
+# the default supported by Net::SMTP so we subclass
+# and override the auth method to support that.
+
+use Net::SMTP;
+use Net::Cmd;
+use MIME::Base64 qw(encode_base64);
+our @ISA = qw(Net::SMTP);
+our $auth_type = ::git_config 'cidaemon.smtpAuth';
+
+sub new
+{
+ my $self = shift;
+ my $type = ref($self) || $self;
+ $type->SUPER::new(@_);
+}
+
+sub auth
+{
+ my $self = shift;
+ return $self->SUPER::auth(@_) unless $auth_type eq 'login';
+
+ my $user = encode_base64 shift, '';
+ my $pass = encode_base64 shift, '';
+ return 0 unless CMD_MORE == $self->command("AUTH LOGIN")->response;
+ return 0 unless CMD_MORE == $self->command($user)->response;
+ CMD_OK == $self->command($pass)->response;
+}
+
+package main;
+
+my ($debug_flag, %recent);
+
+my $ex_host = git_config('cidaemon.smtpHost') || 'localhost';
+my $ex_user = git_config('cidaemon.smtpUser');
+my $ex_pass = git_config('cidaemon.smtpPassword');
+
+my $ex_from_addr = git_config('cidaemon.email', 1);
+my $ex_from_name = git_config('cidaemon.name') || 'cidaemon';
+
+my $scan_delay = git_config('cidaemon.scanDelay') || 60;
+my $recent_size = git_config('cidaemon.recentCache') || 100;
+my $tmpdir = git_config('cidaemon.tmpdir') || '/tmp';
+my $queue_name = git_config('cidaemon.queue', 1);
+my $queue_lock = "$queue_name.lock";
+
+my @nocc_list;
+open GIT,'git config --get-all cidaemon.nocc|';
+while (<GIT>) {
+ chop;
+ push @nocc_list, $_;
+}
+close GIT;
+
+sub nocc_author ($)
+{
+ local $_ = shift;
+ foreach my $pat (@nocc_list) {
+ return 1 if /$pat/;
+ }
+ 0;
+}
+
+sub input_echo ($)
+{
+ my $prompt = shift;
+
+ local $| = 1;
+ print $prompt;
+ my $input = <STDIN>;
+ chop $input;
+ return $input;
+}
+
+sub input_noecho ($)
+{
+ my $prompt = shift;
+
+ my $end = sub {system('stty','echo');print "\n";exit};
+ local $SIG{TERM} = $end;
+ local $SIG{INT} = $end;
+ system('stty','-echo');
+
+ local $| = 1;
+ print $prompt;
+ my $input = <STDIN>;
+ system('stty','echo');
+ print "\n";
+ chop $input;
+ return $input;
+}
+
+sub rfc2822_date ()
+{
+ strftime("%a, %d %b %Y %H:%M:%S %Z", localtime);
+}
+
+sub send_email ($$$)
+{
+ my ($subj, $body, $to) = @_;
+ my $now = rfc2822_date;
+ my $to_str = '';
+ my @rcpt_to;
+ foreach (@$to) {
+ my $s = $_;
+ $s =~ s/^/"/;
+ $s =~ s/(\s+<)/"$1/;
+ $to_str .= ', ' if $to_str;
+ $to_str .= $s;
+ push @rcpt_to, $1 if $s =~ /<(.*)>/;
+ }
+ die "Nobody to send to.\n" unless @rcpt_to;
+ my $msg = <<EOF;
+From: "$ex_from_name" <$ex_from_addr>
+To: $to_str
+Date: $now
+Subject: $subj
+
+$body
+EOF
+
+ my $smtp = EXCHANGE_NET_SMTP->new(Host => $ex_host)
+ or die "Cannot connect to $ex_host: $!\n";
+ if ($ex_user && $ex_pass) {
+ $smtp->auth($ex_user,$ex_pass)
+ or die "$ex_host rejected $ex_user\n";
+ }
+ $smtp->mail($ex_from_addr)
+ or die "$ex_host rejected $ex_from_addr\n";
+ scalar($smtp->recipient(@rcpt_to, { SkipBad => 1 }))
+ or die "$ex_host did not accept any addresses.\n";
+ $smtp->data($msg)
+ or die "$ex_host rejected message data\n";
+ $smtp->quit;
+}
+
+sub pop_queue ()
+{
+ open LOCK, ">$queue_lock" or die "Can't open $queue_lock: $!";
+ flock LOCK, LOCK_EX;
+
+ my $queue = -f $queue_name ? retrieve $queue_name : [];
+ my $ent = shift @$queue;
+ nstore $queue, $queue_name;
+
+ flock LOCK, LOCK_UN;
+ close LOCK;
+ $ent;
+}
+
+sub git_exec (@)
+{
+ system('git',@_) == 0 or die "Cannot git " . join(' ', @_) . "\n";
+}
+
+sub git_val (@)
+{
+ open(C, '-|','git',@_);
+ my $r = <C>;
+ chop $r if $r;
+ close C;
+ $r;
+}
+
+sub do_build ($$)
+{
+ my ($git_dir, $new) = @_;
+
+ my $tmp = File::Spec->catfile($tmpdir, "builder$$");
+ system('rm','-rf',$tmp) == 0 or die "Cannot clear $tmp\n";
+ die "Cannot clear $tmp.\n" if -e $tmp;
+
+ my $result = 1;
+ eval {
+ my $command;
+ {
+ local $ENV{GIT_DIR} = $git_dir;
+ $command = git_val 'config','builder.command';
+ }
+ die "No builder.command for $git_dir.\n" unless $command;
+
+ git_exec 'clone','-n','-l','-s',$git_dir,$tmp;
+ chmod 0700, $tmp or die "Cannot lock $tmp\n";
+ chdir $tmp or die "Cannot enter $tmp\n";
+
+ git_exec 'update-ref','HEAD',$new;
+ git_exec 'read-tree','-m','-u','HEAD','HEAD';
+ system $command;
+ if ($? == -1) {
+ print STDERR "failed to execute '$command': $!\n";
+ $result = 1;
+ } elsif ($? & 127) {
+ my $sig = $? & 127;
+ print STDERR "'$command' died from signal $sig\n";
+ $result = 1;
+ } else {
+ my $r = $? >> 8;
+ print STDERR "'$command' exited with $r\n" if $r;
+ $result = $r;
+ }
+ };
+ if ($@) {
+ $result = 2;
+ print STDERR "$@\n";
+ }
+
+ chdir '/';
+ system('rm','-rf',$tmp);
+ rmdir $tmp;
+ $result;
+}
+
+sub build_failed ($$$$$)
+{
+ my ($git_dir, $ref, $old, $new, $msg) = @_;
+
+ $git_dir =~ m,/([^/]+)$,;
+ my $repo_name = $1;
+ $ref =~ s,^refs/(heads|tags)/,,;
+
+ my %authors;
+ my $shortlog;
+ my $revstr;
+ {
+ local $ENV{GIT_DIR} = $git_dir;
+ my @revs = ($new);
+ push @revs, '--not', @$old if @$old;
+ open LOG,'-|','git','rev-list','--pretty=raw',@revs;
+ while (<LOG>) {
+ if (s/^(author|committer) //) {
+ chomp;
+ s/>.*$/>/;
+ $authors{$_} = 1 unless nocc_author $_;
+ }
+ }
+ close LOG;
+ open LOG,'-|','git','shortlog',@revs;
+ $shortlog .= $_ while <LOG>;
+ close LOG;
+ $revstr = join(' ', @revs);
+ }
+
+ my @to = sort keys %authors;
+ unless (@to) {
+ print STDERR "error: No authors in $revstr\n";
+ return;
+ }
+
+ my $subject = "[$repo_name] $ref : Build Failed";
+ my $body = <<EOF;
+Project: $git_dir
+Branch: $ref
+Commits: $revstr
+
+$shortlog
+Build Output:
+--------------------------------------------------------------
+$msg
+EOF
+ send_email($subject, $body, \@to);
+}
+
+sub run_build ($$$$)
+{
+ my ($git_dir, $ref, $old, $new) = @_;
+
+ if ($debug_flag) {
+ my @revs = ($new);
+ push @revs, '--not', @$old if @$old;
+ print "BUILDING $git_dir\n";
+ print " BRANCH: $ref\n";
+ print " COMMITS: ", join(' ', @revs), "\n";
+ }
+
+ local(*R, *W);
+ pipe R, W or die "cannot pipe builder: $!";
+
+ my $builder = fork();
+ if (!defined $builder) {
+ die "cannot fork builder: $!";
+ } elsif (0 == $builder) {
+ close R;
+ close STDIN;open(STDIN, '/dev/null');
+ open(STDOUT, '>&W');
+ open(STDERR, '>&W');
+ exit do_build $git_dir, $new;
+ } else {
+ close W;
+ my $out = '';
+ $out .= $_ while <R>;
+ close R;
+ waitpid $builder, 0;
+ build_failed $git_dir, $ref, $old, $new, $out if $?;
+ }
+
+ print "DONE\n\n" if $debug_flag;
+}
+
+sub daemon_loop ()
+{
+ my $run = 1;
+ my $stop_sub = sub {$run = 0};
+ $SIG{HUP} = $stop_sub;
+ $SIG{INT} = $stop_sub;
+ $SIG{TERM} = $stop_sub;
+
+ mkdir $tmpdir, 0755;
+ my $pidfile = File::Spec->catfile($tmpdir, "cidaemon.pid");
+ open(O, ">$pidfile"); print O "$$\n"; close O;
+
+ while ($run) {
+ my $ent = pop_queue;
+ if ($ent) {
+ my ($git_dir, $ref, $old, $new) = @$ent;
+
+ $ent = $recent{$git_dir};
+ $recent{$git_dir} = $ent = [[], {}] unless $ent;
+ my ($rec_arr, $rec_hash) = @$ent;
+ next if $rec_hash->{$new}++;
+ while (@$rec_arr >= $recent_size) {
+ my $to_kill = shift @$rec_arr;
+ delete $rec_hash->{$to_kill};
+ }
+ push @$rec_arr, $new;
+
+ run_build $git_dir, $ref, $old, $new;
+ } else {
+ sleep $scan_delay;
+ }
+ }
+
+ unlink $pidfile;
+}
+
+$debug_flag = 0;
+GetOptions(
+ 'debug|d' => \$debug_flag,
+ 'smtp-user=s' => \$ex_user,
+) or die "usage: $0 [--debug] [--smtp-user=user]\n";
+
+$ex_pass = input_noecho("$ex_user SMTP password: ")
+ if ($ex_user && !$ex_pass);
+
+if ($debug_flag) {
+ daemon_loop;
+ exit 0;
+}
+
+my $daemon = fork();
+if (!defined $daemon) {
+ die "cannot fork daemon: $!";
+} elsif (0 == $daemon) {
+ close STDIN;open(STDIN, '/dev/null');
+ close STDOUT;open(STDOUT, '>/dev/null');
+ close STDERR;open(STDERR, '>/dev/null');
+ daemon_loop;
+ exit 0;
+} else {
+ print "Daemon $daemon running in the background.\n";
+}
diff --git a/contrib/continuous/post-receive-cinotify b/contrib/continuous/post-receive-cinotify
new file mode 100644
index 0000000000..b8f5a609af
--- /dev/null
+++ b/contrib/continuous/post-receive-cinotify
@@ -0,0 +1,104 @@
+#!/usr/bin/perl
+#
+# A hook that notifies its companion cidaemon through a simple
+# queue file that a ref has been updated via a push (actually
+# by a receive-pack running on the server).
+#
+# See cidaemon for per-repository configuration details.
+#
+# To use this hook, add it as the post-receive hook, make it
+# executable, and set its configuration options.
+#
+
+local $ENV{PATH} = '/opt/git/bin';
+
+use strict;
+use warnings;
+use File::Spec;
+use Storable qw(retrieve nstore);
+use Fcntl ':flock';
+
+my $git_dir = File::Spec->rel2abs($ENV{GIT_DIR});
+my $queue_name = `git config --get builder.queue`;chop $queue_name;
+$queue_name =~ m,^([^\s]+)$,; $queue_name = $1; # untaint
+unless ($queue_name) {
+ 1 while <STDIN>;
+ print STDERR "\nerror: builder.queue not set. Not enqueing.\n\n";
+ exit;
+}
+my $queue_lock = "$queue_name.lock";
+
+my @skip;
+open S, "git config --get-all builder.skip|";
+while (<S>) {
+ chop;
+ push @skip, $_;
+}
+close S;
+
+my @new_branch_base;
+open S, "git config --get-all builder.newBranchBase|";
+while (<S>) {
+ chop;
+ push @new_branch_base, $_;
+}
+close S;
+
+sub skip ($)
+{
+ local $_ = shift;
+ foreach my $p (@skip) {
+ return 1 if /^$p/;
+ }
+ 0;
+}
+
+open LOCK, ">$queue_lock" or die "Can't open $queue_lock: $!";
+flock LOCK, LOCK_EX;
+
+my $queue = -f $queue_name ? retrieve $queue_name : [];
+my %existing;
+foreach my $r (@$queue) {
+ my ($gd, $ref) = @$r;
+ $existing{$gd}{$ref} = $r;
+}
+
+my @new_branch_commits;
+my $loaded_new_branch_commits = 0;
+
+while (<STDIN>) {
+ chop;
+ my ($old, $new, $ref) = split / /, $_, 3;
+
+ next if $old eq $new;
+ next if $new =~ /^0{40}$/;
+ next if skip $ref;
+
+ my $r = $existing{$git_dir}{$ref};
+ if ($r) {
+ $r->[3] = $new;
+ } else {
+ if ($old =~ /^0{40}$/) {
+ if (!$loaded_new_branch_commits && @new_branch_base) {
+ open M,'-|','git','show-ref',@new_branch_base;
+ while (<M>) {
+ ($_) = split / /, $_;
+ push @new_branch_commits, $_;
+ }
+ close M;
+ $loaded_new_branch_commits = 1;
+ }
+ $old = [@new_branch_commits];
+ } else {
+ $old = [$old];
+ }
+
+ $r = [$git_dir, $ref, $old, $new];
+ $existing{$git_dir}{$ref} = $r;
+ push @$queue, $r;
+ }
+}
+nstore $queue, $queue_name;
+
+flock LOCK, LOCK_UN;
+close LOCK;
diff --git a/contrib/emacs/.gitignore b/contrib/emacs/.gitignore
new file mode 100644
index 0000000000..c531d9867f
--- /dev/null
+++ b/contrib/emacs/.gitignore
@@ -0,0 +1 @@
+*.elc
diff --git a/contrib/emacs/Makefile b/contrib/emacs/Makefile
new file mode 100644
index 0000000000..98aa0aae9b
--- /dev/null
+++ b/contrib/emacs/Makefile
@@ -0,0 +1,20 @@
+## Build and install stuff
+
+EMACS = emacs
+
+ELC = git.elc vc-git.elc git-blame.elc
+INSTALL ?= install
+INSTALL_ELC = $(INSTALL) -m 644
+prefix ?= $(HOME)
+emacsdir = $(prefix)/share/emacs/site-lisp
+
+all: $(ELC)
+
+install: all
+ $(INSTALL) -d $(DESTDIR)$(emacsdir)
+ $(INSTALL_ELC) $(ELC) $(DESTDIR)$(emacsdir)
+
+%.elc: %.el
+ $(EMACS) -batch -f batch-byte-compile $<
+
+clean:; rm -f $(ELC)
diff --git a/contrib/emacs/git-blame.el b/contrib/emacs/git-blame.el
new file mode 100644
index 0000000000..64ad50b327
--- /dev/null
+++ b/contrib/emacs/git-blame.el
@@ -0,0 +1,380 @@
+;;; git-blame.el --- Minor mode for incremental blame for Git -*- coding: utf-8 -*-
+;;
+;; Copyright (C) 2007 David KÃ¥gedal
+;;
+;; Authors: David KÃ¥gedal <davidk@lysator.liu.se>
+;; Created: 31 Jan 2007
+;; Message-ID: <87iren2vqx.fsf@morpheus.local>
+;; License: GPL
+;; Keywords: git, version control, release management
+;;
+;; Compatibility: Emacs21
+
+
+;; This file is *NOT* part of GNU Emacs.
+;; This file is distributed under the same terms as GNU Emacs.
+
+;; 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
+
+;; http://www.fsf.org/copyleft/gpl.html
+
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;
+;;; Commentary:
+;;
+;; Here is an Emacs implementation of incremental git-blame. When you
+;; turn it on while viewing a file, the editor buffer will be updated by
+;; setting the background of individual lines to a color that reflects
+;; which commit it comes from. And when you move around the buffer, a
+;; one-line summary will be shown in the echo area.
+
+;;; Installation:
+;;
+;; To use this package, put it somewhere in `load-path' (or add
+;; directory with git-blame.el to `load-path'), and add the following
+;; line to your .emacs:
+;;
+;; (require 'git-blame)
+;;
+;; If you do not want to load this package before it is necessary, you
+;; can make use of the `autoload' feature, e.g. by adding to your .emacs
+;; the following lines
+;;
+;; (autoload 'git-blame-mode "git-blame"
+;; "Minor mode for incremental blame for Git." t)
+;;
+;; Then first use of `M-x git-blame-mode' would load the package.
+
+;;; Compatibility:
+;;
+;; It requires GNU Emacs 21. If you'are using Emacs 20, try
+;; changing this:
+;;
+;; (overlay-put ovl 'face (list :background
+;; (cdr (assq 'color (cddddr info)))))
+;;
+;; to
+;;
+;; (overlay-put ovl 'face (cons 'background-color
+;; (cdr (assq 'color (cddddr info)))))
+
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;
+;;; Code:
+
+(require 'cl) ; to use `push', `pop'
+
+(defun color-scale (l)
+ (let* ((colors ())
+ r g b)
+ (setq r l)
+ (while r
+ (setq g l)
+ (while g
+ (setq b l)
+ (while b
+ (push (concat "#" (car r) (car g) (car b)) colors)
+ (pop b))
+ (pop g))
+ (pop r))
+ colors))
+
+(defvar git-blame-dark-colors
+ (color-scale '("0c" "04" "24" "1c" "2c" "34" "14" "3c")))
+
+(defvar git-blame-light-colors
+ (color-scale '("c4" "d4" "cc" "dc" "f4" "e4" "fc" "ec")))
+
+(defvar git-blame-ancient-color "dark green")
+
+(defvar git-blame-autoupdate t
+ "*Automatically update the blame display while editing")
+
+(defvar git-blame-proc nil
+ "The running git-blame process")
+(make-variable-buffer-local 'git-blame-proc)
+
+(defvar git-blame-overlays nil
+ "The git-blame overlays used in the current buffer.")
+(make-variable-buffer-local 'git-blame-overlays)
+
+(defvar git-blame-cache nil
+ "A cache of git-blame information for the current buffer")
+(make-variable-buffer-local 'git-blame-cache)
+
+(defvar git-blame-idle-timer nil
+ "An idle timer that updates the blame")
+(make-variable-buffer-local 'git-blame-cache)
+
+(defvar git-blame-update-queue nil
+ "A queue of update requests")
+(make-variable-buffer-local 'git-blame-update-queue)
+
+(defvar git-blame-mode nil)
+(make-variable-buffer-local 'git-blame-mode)
+(unless (assq 'git-blame-mode minor-mode-alist)
+ (setq minor-mode-alist
+ (cons (list 'git-blame-mode " blame")
+ minor-mode-alist)))
+
+;;;###autoload
+(defun git-blame-mode (&optional arg)
+ "Minor mode for displaying Git blame"
+ (interactive "P")
+ (if arg
+ (setq git-blame-mode (eq arg 1))
+ (setq git-blame-mode (not git-blame-mode)))
+ (make-local-variable 'git-blame-colors)
+ (if git-blame-autoupdate
+ (add-hook 'after-change-functions 'git-blame-after-change nil t)
+ (remove-hook 'after-change-functions 'git-blame-after-change t))
+ (git-blame-cleanup)
+ (if git-blame-mode
+ (progn
+ (let ((bgmode (cdr (assoc 'background-mode (frame-parameters)))))
+ (if (eq bgmode 'dark)
+ (setq git-blame-colors git-blame-dark-colors)
+ (setq git-blame-colors git-blame-light-colors)))
+ (setq git-blame-cache (make-hash-table :test 'equal))
+ (git-blame-run))
+ (cancel-timer git-blame-idle-timer)))
+
+;;;###autoload
+(defun git-reblame ()
+ "Recalculate all blame information in the current buffer"
+ (unless git-blame-mode
+ (error "git-blame is not active"))
+ (interactive)
+ (git-blame-cleanup)
+ (git-blame-run))
+
+(defun git-blame-run (&optional startline endline)
+ (if git-blame-proc
+ ;; Should maybe queue up a new run here
+ (message "Already running git blame")
+ (let ((display-buf (current-buffer))
+ (blame-buf (get-buffer-create
+ (concat " git blame for " (buffer-name))))
+ (args '("--incremental" "--contents" "-")))
+ (if startline
+ (setq args (append args
+ (list "-L" (format "%d,%d" startline endline)))))
+ (setq args (append args
+ (list (file-name-nondirectory buffer-file-name))))
+ (setq git-blame-proc
+ (apply 'start-process
+ "git-blame" blame-buf
+ "git" "blame"
+ args))
+ (with-current-buffer blame-buf
+ (erase-buffer)
+ (make-local-variable 'git-blame-file)
+ (make-local-variable 'git-blame-current)
+ (setq git-blame-file display-buf)
+ (setq git-blame-current nil))
+ (set-process-filter git-blame-proc 'git-blame-filter)
+ (set-process-sentinel git-blame-proc 'git-blame-sentinel)
+ (process-send-region git-blame-proc (point-min) (point-max))
+ (process-send-eof git-blame-proc))))
+
+(defun remove-git-blame-text-properties (start end)
+ (let ((modified (buffer-modified-p))
+ (inhibit-read-only t))
+ (remove-text-properties start end '(point-entered nil))
+ (set-buffer-modified-p modified)))
+
+(defun git-blame-cleanup ()
+ "Remove all blame properties"
+ (mapcar 'delete-overlay git-blame-overlays)
+ (setq git-blame-overlays nil)
+ (remove-git-blame-text-properties (point-min) (point-max)))
+
+(defun git-blame-update-region (start end)
+ "Rerun blame to get updates between START and END"
+ (let ((overlays (overlays-in start end)))
+ (while overlays
+ (let ((overlay (pop overlays)))
+ (if (< (overlay-start overlay) start)
+ (setq start (overlay-start overlay)))
+ (if (> (overlay-end overlay) end)
+ (setq end (overlay-end overlay)))
+ (setq git-blame-overlays (delete overlay git-blame-overlays))
+ (delete-overlay overlay))))
+ (remove-git-blame-text-properties start end)
+ ;; We can be sure that start and end are at line breaks
+ (git-blame-run (1+ (count-lines (point-min) start))
+ (count-lines (point-min) end)))
+
+(defun git-blame-sentinel (proc status)
+ (with-current-buffer (process-buffer proc)
+ (with-current-buffer git-blame-file
+ (setq git-blame-proc nil)
+ (if git-blame-update-queue
+ (git-blame-delayed-update))))
+ ;;(kill-buffer (process-buffer proc))
+ ;;(message "git blame finished")
+ )
+
+(defvar in-blame-filter nil)
+
+(defun git-blame-filter (proc str)
+ (save-excursion
+ (set-buffer (process-buffer proc))
+ (goto-char (process-mark proc))
+ (insert-before-markers str)
+ (goto-char 0)
+ (unless in-blame-filter
+ (let ((more t)
+ (in-blame-filter t))
+ (while more
+ (setq more (git-blame-parse)))))))
+
+(defun git-blame-parse ()
+ (cond ((looking-at "\\([0-9a-f]\\{40\\}\\) \\([0-9]+\\) \\([0-9]+\\) \\([0-9]+\\)\n")
+ (let ((hash (match-string 1))
+ (src-line (string-to-number (match-string 2)))
+ (res-line (string-to-number (match-string 3)))
+ (num-lines (string-to-number (match-string 4))))
+ (setq git-blame-current
+ (if (string= hash "0000000000000000000000000000000000000000")
+ nil
+ (git-blame-new-commit
+ hash src-line res-line num-lines))))
+ (delete-region (point) (match-end 0))
+ t)
+ ((looking-at "filename \\(.+\\)\n")
+ (let ((filename (match-string 1)))
+ (git-blame-add-info "filename" filename))
+ (delete-region (point) (match-end 0))
+ t)
+ ((looking-at "\\([a-z-]+\\) \\(.+\\)\n")
+ (let ((key (match-string 1))
+ (value (match-string 2)))
+ (git-blame-add-info key value))
+ (delete-region (point) (match-end 0))
+ t)
+ ((looking-at "boundary\n")
+ (setq git-blame-current nil)
+ (delete-region (point) (match-end 0))
+ t)
+ (t
+ nil)))
+
+
+(defun git-blame-new-commit (hash src-line res-line num-lines)
+ (save-excursion
+ (set-buffer git-blame-file)
+ (let ((info (gethash hash git-blame-cache))
+ (inhibit-point-motion-hooks t)
+ (inhibit-modification-hooks t))
+ (when (not info)
+ (let ((color (pop git-blame-colors)))
+ (unless color
+ (setq color git-blame-ancient-color))
+ (setq info (list hash src-line res-line num-lines
+ (git-describe-commit hash)
+ (cons 'color color))))
+ (puthash hash info git-blame-cache))
+ (goto-line res-line)
+ (while (> num-lines 0)
+ (if (get-text-property (point) 'git-blame)
+ (forward-line)
+ (let* ((start (point))
+ (end (progn (forward-line 1) (point)))
+ (ovl (make-overlay start end)))
+ (push ovl git-blame-overlays)
+ (overlay-put ovl 'git-blame info)
+ (overlay-put ovl 'help-echo hash)
+ (overlay-put ovl 'face (list :background
+ (cdr (assq 'color (nthcdr 5 info)))))
+ ;; the point-entered property doesn't seem to work in overlays
+ ;;(overlay-put ovl 'point-entered
+ ;; `(lambda (x y) (git-blame-identify ,hash)))
+ (let ((modified (buffer-modified-p)))
+ (put-text-property (if (= start 1) start (1- start)) (1- end)
+ 'point-entered
+ `(lambda (x y) (git-blame-identify ,hash)))
+ (set-buffer-modified-p modified))))
+ (setq num-lines (1- num-lines))))))
+
+(defun git-blame-add-info (key value)
+ (if git-blame-current
+ (nconc git-blame-current (list (cons (intern key) value)))))
+
+(defun git-blame-current-commit ()
+ (let ((info (get-char-property (point) 'git-blame)))
+ (if info
+ (car info)
+ (error "No commit info"))))
+
+(defun git-describe-commit (hash)
+ (with-temp-buffer
+ (call-process "git" nil t nil
+ "log" "-1" "--pretty=oneline"
+ hash)
+ (buffer-substring (point-min) (1- (point-max)))))
+
+(defvar git-blame-last-identification nil)
+(make-variable-buffer-local 'git-blame-last-identification)
+(defun git-blame-identify (&optional hash)
+ (interactive)
+ (let ((info (gethash (or hash (git-blame-current-commit)) git-blame-cache)))
+ (when (and info (not (eq info git-blame-last-identification)))
+ (message "%s" (nth 4 info))
+ (setq git-blame-last-identification info))))
+
+;; (defun git-blame-after-save ()
+;; (when git-blame-mode
+;; (git-blame-cleanup)
+;; (git-blame-run)))
+;; (add-hook 'after-save-hook 'git-blame-after-save)
+
+(defun git-blame-after-change (start end length)
+ (when git-blame-mode
+ (git-blame-enq-update start end)))
+
+(defvar git-blame-last-update nil)
+(make-variable-buffer-local 'git-blame-last-update)
+(defun git-blame-enq-update (start end)
+ "Mark the region between START and END as needing blame update"
+ ;; Try to be smart and avoid multiple callouts for sequential
+ ;; editing
+ (cond ((and git-blame-last-update
+ (= start (cdr git-blame-last-update)))
+ (setcdr git-blame-last-update end))
+ ((and git-blame-last-update
+ (= end (car git-blame-last-update)))
+ (setcar git-blame-last-update start))
+ (t
+ (setq git-blame-last-update (cons start end))
+ (setq git-blame-update-queue (nconc git-blame-update-queue
+ (list git-blame-last-update)))))
+ (unless (or git-blame-proc git-blame-idle-timer)
+ (setq git-blame-idle-timer
+ (run-with-idle-timer 0.5 nil 'git-blame-delayed-update))))
+
+(defun git-blame-delayed-update ()
+ (setq git-blame-idle-timer nil)
+ (if git-blame-update-queue
+ (let ((first (pop git-blame-update-queue))
+ (inhibit-point-motion-hooks t))
+ (git-blame-update-region (car first) (cdr first)))))
+
+(provide 'git-blame)
+
+;;; git-blame.el ends here
diff --git a/contrib/emacs/git.el b/contrib/emacs/git.el
new file mode 100644
index 0000000000..2f9995ea39
--- /dev/null
+++ b/contrib/emacs/git.el
@@ -0,0 +1,1203 @@
+;;; git.el --- A user interface for git
+
+;; Copyright (C) 2005, 2006, 2007 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
+;; - creating tags
+;; - fetch/pull
+;; - switching branches
+;; - revlist browser
+;; - git-show-branch browser
+;; - menus
+;;
+
+(eval-when-compile (require 'cl))
+(require 'ewoc)
+(require 'log-edit)
+
+
+;;;; Customizations
+;;;; ------------------------------------------------------------
+
+(defgroup git nil
+ "A user interface for the git versioning system."
+ :group 'tools)
+
+(defcustom git-committer-name nil
+ "User name to use for commits.
+The default is to fall back to the repository config,
+then to `add-log-full-name' and then to `user-full-name'."
+ :group 'git
+ :type '(choice (const :tag "Default" nil)
+ (string :tag "Name")))
+
+(defcustom git-committer-email nil
+ "Email address to use for commits.
+The default is to fall back to the git repository config,
+then to `add-log-mailing-address' and then to `user-mail-address'."
+ :group 'git
+ :type '(choice (const :tag "Default" nil)
+ (string :tag "Email")))
+
+(defcustom git-commits-coding-system nil
+ "Default coding system for the log message of git commits."
+ :group 'git
+ :type '(choice (const :tag "From repository config" nil)
+ (coding-system)))
+
+(defcustom git-append-signed-off-by nil
+ "Whether to append a Signed-off-by line to the commit message before editing."
+ :group 'git
+ :type 'boolean)
+
+(defcustom git-reuse-status-buffer t
+ "Whether `git-status' should try to reuse an existing buffer
+if there is already one that displays the same directory."
+ :group 'git
+ :type 'boolean)
+
+(defcustom git-per-dir-ignore-file ".gitignore"
+ "Name of the per-directory ignore file."
+ :group 'git
+ :type 'string)
+
+
+(defface git-status-face
+ '((((class color) (background light)) (:foreground "purple")))
+ "Git mode face used to highlight added and modified files."
+ :group 'git)
+
+(defface git-unmerged-face
+ '((((class color) (background light)) (:foreground "red" :bold t)))
+ "Git mode face used to highlight unmerged files."
+ :group 'git)
+
+(defface git-unknown-face
+ '((((class color) (background light)) (:foreground "goldenrod" :bold t)))
+ "Git mode face used to highlight unknown files."
+ :group 'git)
+
+(defface git-uptodate-face
+ '((((class color) (background light)) (:foreground "grey60")))
+ "Git mode face used to highlight up-to-date files."
+ :group 'git)
+
+(defface git-ignored-face
+ '((((class color) (background light)) (:foreground "grey60")))
+ "Git mode face used to highlight ignored files."
+ :group 'git)
+
+(defface git-mark-face
+ '((((class color) (background light)) (:foreground "red" :bold t)))
+ "Git mode face used for the file marks."
+ :group 'git)
+
+(defface git-header-face
+ '((((class color) (background light)) (:foreground "blue")))
+ "Git mode face used for commit headers."
+ :group 'git)
+
+(defface git-separator-face
+ '((((class color) (background light)) (:foreground "brown")))
+ "Git mode face used for commit separator."
+ :group 'git)
+
+(defface git-permission-face
+ '((((class color) (background light)) (:foreground "green" :bold t)))
+ "Git mode face used for permission changes."
+ :group 'git)
+
+
+;;;; Utilities
+;;;; ------------------------------------------------------------
+
+(defconst git-log-msg-separator "--- log message follows this line ---")
+
+(defvar git-log-edit-font-lock-keywords
+ `(("^\\(Author:\\|Date:\\|Parent:\\|Signed-off-by:\\)\\(.*\\)$"
+ (1 font-lock-keyword-face)
+ (2 font-lock-function-name-face))
+ (,(concat "^\\(" (regexp-quote git-log-msg-separator) "\\)$")
+ (1 font-lock-comment-face))))
+
+(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-call-process-env-string (env &rest args)
+ "Wrapper for call-process that sets environment strings,
+and returns the process output as a string."
+ (with-temp-buffer
+ (and (eq 0 (apply #' git-call-process-env t env args))
+ (buffer-string))))
+
+(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-run-hook (hook env &rest args)
+ "Run a git hook and display its output if any."
+ (let ((dir default-directory)
+ (hook-name (expand-file-name (concat ".git/hooks/" hook))))
+ (or (not (file-executable-p hook-name))
+ (let (status (buffer (get-buffer-create "*Git Hook Output*")))
+ (with-current-buffer buffer
+ (erase-buffer)
+ (cd dir)
+ (setq status
+ (if env
+ (apply #'call-process "env" nil (list buffer t) nil
+ (append (git-get-env-strings env) (list hook-name) args))
+ (apply #'call-process hook-name nil (list buffer t) nil args))))
+ (display-message-or-buffer buffer)
+ (eq 0 status)))))
+
+(defun git-get-string-sha1 (string)
+ "Read a SHA1 from the specified string."
+ (and string
+ (string-match "[0-9a-f]\\{40\\}" string)
+ (match-string 0 string)))
+
+(defun git-get-committer-name ()
+ "Return the name to use as GIT_COMMITTER_NAME."
+ ; copied from log-edit
+ (or git-committer-name
+ (git-config "user.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
+ (git-config "user.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-get-commits-coding-system ()
+ "Return the coding system to use for commits."
+ (let ((repo-config (git-config "i18n.commitencoding")))
+ (or git-commits-coding-system
+ (and repo-config
+ (fboundp 'locale-charset-to-coding-system)
+ (locale-charset-to-coding-system repo-config))
+ 'utf-8)))
+
+(defun git-get-logoutput-coding-system ()
+ "Return the coding system used for git-log output."
+ (let ((repo-config (or (git-config "i18n.logoutputencoding")
+ (git-config "i18n.commitencoding"))))
+ (or git-commits-coding-system
+ (and repo-config
+ (fboundp 'locale-charset-to-coding-system)
+ (locale-charset-to-coding-system repo-config))
+ 'utf-8)))
+
+(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))
+ (ignore-name (expand-file-name git-per-dir-ignore-file dir))
+ (created (not (file-exists-p ignore-name))))
+ (save-window-excursion
+ (set-buffer (find-file-noselect ignore-name))
+ (goto-char (point-max))
+ (unless (zerop (current-column)) (insert "\n"))
+ (insert "/" name "\n")
+ (sort-lines nil (point-min) (point-max))
+ (save-buffer))
+ (when created
+ (git-run-command nil nil "update-index" "--info-only" "--add" "--" (file-relative-name ignore-name)))
+ (git-add-status-file (if created 'added 'modified) (file-relative-name ignore-name))))
+
+; propertize definition for XEmacs, stolen from erc-compat
+(eval-when-compile
+ (unless (fboundp 'propertize)
+ (defun propertize (string &rest props)
+ (let ((string (copy-sequence string)))
+ (while props
+ (put-text-property 0 (length string) (nth 0 props) (nth 1 props) string)
+ (setq props (cddr props)))
+ string))))
+
+;;;; Wrappers for basic git commands
+;;;; ------------------------------------------------------------
+
+(defun git-rev-parse (rev)
+ "Parse a revision name and return its SHA1."
+ (git-get-string-sha1
+ (git-call-process-env-string nil "rev-parse" rev)))
+
+(defun git-config (key)
+ "Retrieve the value associated to KEY in the git repository config file."
+ (let ((str (git-call-process-env-string nil "config" key)))
+ (and str (car (split-string str "\n")))))
+
+(defun git-symbolic-ref (ref)
+ "Wrapper for the git-symbolic-ref command."
+ (let ((str (git-call-process-env-string nil "symbolic-ref" ref)))
+ (and str (car (split-string str "\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
+ (git-call-process-env-string (and index-file `(("GIT_INDEX_FILE" . ,index-file))) "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 coding-system-for-write)
+ (when head
+ (push "-p" args)
+ (push head args))
+ (with-current-buffer buffer
+ (goto-char (point-min))
+ (if
+ (setq log-start (re-search-forward (concat "^" (regexp-quote 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))
+ (setq coding-system-for-write buffer-file-coding-system))
+ (git-get-string-sha1
+ (with-output-to-string
+ (with-current-buffer standard-output
+ (let ((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)))
+
+(defun git-get-commit-description (commit)
+ "Get a one-line description of COMMIT."
+ (let ((coding-system-for-read (git-get-logoutput-coding-system)))
+ (let ((descr (git-call-process-env-string nil "log" "--max-count=1" "--pretty=oneline" commit)))
+ (if (and descr (string-match "\\`\\([0-9a-f]\\{40\\}\\) *\\(.*\\)$" descr))
+ (concat (substring (match-string 1 descr) 0 10) " - " (match-string 2 descr))
+ descr))))
+
+;;;; 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 from "
+ "renamed to "))
+ (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 ?\111 (logxor old-perm new-perm))))
+ " "
+ (if (eq 0 (logand ?\111 old-perm)) "+x" "-x"))
+ 'face 'git-permission-face))
+
+(defun git-fileinfo-prettyprint (info)
+ "Pretty-printer for the git-fileinfo structure."
+ (insert (concat " " (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"
+ (git-get-commit-description "HEAD")))
+ (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) (git-get-commit-description str)) merge-heads "\n "))
+ ""))
+ (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-run-pre-commit-hook ()
+ "Run the pre-commit hook if any."
+ (unless git-status (error "Not in git-status buffer."))
+ (let ((files (git-marked-files-state 'added 'deleted 'modified)))
+ (or (not files)
+ (not (file-executable-p ".git/hooks/pre-commit"))
+ (let ((index-file (make-temp-file "gitidx")))
+ (unwind-protect
+ (let ((head-tree (unless (git-empty-db-p) (git-rev-parse "HEAD^{tree}"))))
+ (git-read-tree head-tree index-file)
+ (git-update-index index-file files)
+ (git-run-hook "pre-commit" `(("GIT_INDEX_FILE" . ,index-file))))
+ (delete-file index-file))))))
+
+(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))
+ (condition-case nil (delete-file ".git/MERGE_MSG") (error nil))
+ (with-current-buffer buffer (erase-buffer))
+ (git-set-files-state files 'uptodate)
+ (when (file-directory-p ".git/rr-cache")
+ (git-run-command nil nil "rerere"))
+ (git-refresh-files)
+ (git-refresh-ewoc-hf git-status)
+ (message "Committed %s." commit)
+ (git-run-hook "post-commit" nil))
+ (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-next-unmerged-file (&optional n)
+ "Move the selection down N unmerged files."
+ (interactive "p")
+ (unless git-status (error "Not in git-status buffer."))
+ (let* ((last (ewoc-locate git-status))
+ (node (ewoc-next git-status last)))
+ (while (and node (> n 0))
+ (when (eq 'unmerged (git-fileinfo->state (ewoc-data node)))
+ (setq n (1- n))
+ (setq last node))
+ (setq node (ewoc-next git-status node)))
+ (ewoc-goto-node git-status last)))
+
+(defun git-prev-unmerged-file (&optional n)
+ "Move the selection up N unmerged files."
+ (interactive "p")
+ (unless git-status (error "Not in git-status buffer."))
+ (let* ((last (ewoc-locate git-status))
+ (node (ewoc-prev git-status last)))
+ (while (and node (> n 0))
+ (when (eq 'unmerged (git-fileinfo->state (ewoc-data node)))
+ (setq n (1- n))
+ (setq last node))
+ (setq node (ewoc-prev git-status node)))
+ (ewoc-goto-node git-status last)))
+
+(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" "--" (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-file-merge-head (arg)
+ "Diff the marked file(s) against the first merge head (or the nth one with a numeric prefix)."
+ (interactive "p")
+ (let ((files (git-marked-files))
+ (merge-heads (git-get-merge-heads)))
+ (unless merge-heads (error "No merge in progress"))
+ (git-setup-diff-buffer
+ (apply #'git-run-command-buffer "*git-diff*" "diff-index" "-p" "-M"
+ (or (nth (1- arg) merge-heads) "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-append-sign-off (name email)
+ "Append a Signed-off-by entry to the current buffer, avoiding duplicates."
+ (let ((sign-off (format "Signed-off-by: %s <%s>" name email))
+ (case-fold-search t))
+ (goto-char (point-min))
+ (unless (re-search-forward (concat "^" (regexp-quote sign-off)) nil t)
+ (goto-char (point-min))
+ (unless (re-search-forward "^Signed-off-by: " nil t)
+ (setq sign-off (concat "\n" sign-off)))
+ (goto-char (point-max))
+ (insert sign-off "\n"))))
+
+(defun git-setup-log-buffer (buffer &optional author-name author-email subject date msg)
+ "Setup the log buffer for a commit."
+ (unless git-status (error "Not in git-status buffer."))
+ (let ((merge-heads (git-get-merge-heads))
+ (dir default-directory)
+ (committer-name (git-get-committer-name))
+ (committer-email (git-get-committer-email))
+ (sign-off git-append-signed-off-by))
+ (with-current-buffer buffer
+ (cd dir)
+ (erase-buffer)
+ (insert
+ (propertize
+ (format "Author: %s <%s>\n%s%s"
+ (or author-name committer-name)
+ (or author-email committer-email)
+ (if date (format "Date: %s\n" date) "")
+ (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 subject (insert subject "\n\n"))
+ (cond (msg (insert msg "\n"))
+ ((file-readable-p ".dotest/msg")
+ (insert-file-contents ".dotest/msg"))
+ ((file-readable-p ".git/MERGE_MSG")
+ (insert-file-contents ".git/MERGE_MSG")))
+ ; delete empty lines at end
+ (goto-char (point-min))
+ (when (re-search-forward "\n+\\'" nil t)
+ (replace-match "\n" t t))
+ (when sign-off (git-append-sign-off committer-name committer-email)))))
+
+(defun git-commit-file ()
+ "Commit the marked file(s), asking for a commit message."
+ (interactive)
+ (unless git-status (error "Not in git-status buffer."))
+ (when (git-run-pre-commit-hook)
+ (let ((buffer (get-buffer-create "*git-commit*"))
+ (coding-system (git-get-commits-coding-system))
+ author-name author-email subject date)
+ (when (eq 0 (buffer-size buffer))
+ (when (file-readable-p ".dotest/info")
+ (with-temp-buffer
+ (insert-file-contents ".dotest/info")
+ (goto-char (point-min))
+ (when (re-search-forward "^Author: \\(.*\\)\nEmail: \\(.*\\)$" nil t)
+ (setq author-name (match-string 1))
+ (setq author-email (match-string 2)))
+ (goto-char (point-min))
+ (when (re-search-forward "^Subject: \\(.*\\)$" nil t)
+ (setq subject (match-string 1)))
+ (goto-char (point-min))
+ (when (re-search-forward "^Date: \\(.*\\)$" nil t)
+ (setq date (match-string 1)))))
+ (git-setup-log-buffer buffer author-name author-email subject date))
+ (log-edit #'git-do-commit nil #'git-log-edit-files buffer)
+ (setq font-lock-keywords (font-lock-compile-keywords git-log-edit-font-lock-keywords))
+ (setq buffer-file-coding-system coding-system)
+ (re-search-forward (regexp-quote (concat git-log-msg-separator "\n")) nil t))))
+
+(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-other-window ()
+ "Visit the current file in its own buffer in another window."
+ (interactive)
+ (unless git-status (error "Not in git-status buffer."))
+ (let ((info (ewoc-data (ewoc-locate git-status))))
+ (find-file-other-window (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-help)
+ (define-key map "h" 'git-help)
+ (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 "\r" '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 "N" 'git-next-unmerged-file)
+ (define-key map "o" 'git-find-file-other-window)
+ (define-key map "p" 'git-prev-file)
+ (define-key map "P" 'git-prev-unmerged-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 "h" 'git-diff-file-merge-head)
+ (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))
+ (set (make-local-variable 'list-buffers-directory) default-directory)
+ (run-hooks 'git-status-mode-hook)))
+
+(defun git-find-status-buffer (dir)
+ "Find the git status buffer handling a specified directory."
+ (let ((list (buffer-list))
+ (fulldir (expand-file-name dir))
+ found)
+ (while (and list (not found))
+ (let ((buffer (car list)))
+ (with-current-buffer buffer
+ (when (and list-buffers-directory
+ (string-equal fulldir (expand-file-name list-buffers-directory))
+ (string-match "\\*git-status\\*$" (buffer-name buffer)))
+ (setq found buffer))))
+ (setq list (cdr list)))
+ found))
+
+(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 (or (and git-reuse-status-buffer (git-find-status-buffer dir))
+ (create-file-buffer (expand-file-name "*git-status*" dir)))))
+ (switch-to-buffer buffer)
+ (cd dir)
+ (git-status-mode)
+ (git-refresh-status)
+ (goto-char (point-min)))
+ (message "%s is not a git working tree." dir)))
+
+(defun git-help ()
+ "Display help for Git mode."
+ (interactive)
+ (describe-function 'git-status-mode))
+
+(provide 'git)
+;;; git.el ends here
diff --git a/contrib/emacs/vc-git.el b/contrib/emacs/vc-git.el
new file mode 100644
index 0000000000..e456ab9712
--- /dev/null
+++ b/contrib/emacs/vc-git.el
@@ -0,0 +1,151 @@
+;;; vc-git.el --- VC backend for the git version control system
+
+;; Copyright (C) 2006 Alexandre Julliard
+
+;; 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 a VC backend for the git version control
+;; system.
+;;
+;; To install: put this file on the load-path and add GIT to the list
+;; of supported backends in `vc-handled-backends'; the following line,
+;; placed in your ~/.emacs, will accomplish this:
+;;
+;; (add-to-list 'vc-handled-backends 'GIT)
+;;
+;; TODO
+;; - changelog generation
+;; - working with revisions other than HEAD
+;;
+
+(eval-when-compile (require 'cl))
+
+(defvar git-commits-coding-system 'utf-8
+ "Default coding system for git commits.")
+
+(defun vc-git--run-command-string (file &rest args)
+ "Run a git command on FILE and return its output as string."
+ (let* ((ok t)
+ (str (with-output-to-string
+ (with-current-buffer standard-output
+ (unless (eq 0 (apply #'call-process "git" nil '(t nil) nil
+ (append args (list (file-relative-name file)))))
+ (setq ok nil))))))
+ (and ok str)))
+
+(defun vc-git--run-command (file &rest args)
+ "Run a git command on FILE, discarding any output."
+ (let ((name (file-relative-name file)))
+ (eq 0 (apply #'call-process "git" nil (get-buffer "*Messages") nil (append args (list name))))))
+
+(defun vc-git-registered (file)
+ "Check whether FILE is registered with git."
+ (with-temp-buffer
+ (let* ((dir (file-name-directory file))
+ (name (file-relative-name file dir)))
+ (and (ignore-errors
+ (when dir (cd dir))
+ (eq 0 (call-process "git" nil '(t nil) nil "ls-files" "-c" "-z" "--" name)))
+ (let ((str (buffer-string)))
+ (and (> (length str) (length name))
+ (string= (substring str 0 (1+ (length name))) (concat name "\0"))))))))
+
+(defun vc-git-state (file)
+ "git-specific version of `vc-state'."
+ (let ((diff (vc-git--run-command-string file "diff-index" "-z" "HEAD" "--")))
+ (if (and diff (string-match ":[0-7]\\{6\\} [0-7]\\{6\\} [0-9a-f]\\{40\\} [0-9a-f]\\{40\\} [ADMU]\0[^\0]+\0" diff))
+ 'edited
+ 'up-to-date)))
+
+(defun vc-git-workfile-version (file)
+ "git-specific version of `vc-workfile-version'."
+ (let ((str (with-output-to-string
+ (with-current-buffer standard-output
+ (call-process "git" nil '(t nil) nil "symbolic-ref" "HEAD")))))
+ (if (string-match "^\\(refs/heads/\\)?\\(.+\\)$" str)
+ (match-string 2 str)
+ str)))
+
+(defun vc-git-revert (file &optional contents-done)
+ "Revert FILE to the version stored in the git repository."
+ (if contents-done
+ (vc-git--run-command file "update-index" "--")
+ (vc-git--run-command file "checkout" "HEAD")))
+
+(defun vc-git-checkout-model (file)
+ 'implicit)
+
+(defun vc-git-workfile-unchanged-p (file)
+ (let ((sha1 (vc-git--run-command-string file "hash-object" "--"))
+ (head (vc-git--run-command-string file "ls-tree" "-z" "HEAD" "--")))
+ (and head
+ (string-match "[0-7]\\{6\\} blob \\([0-9a-f]\\{40\\}\\)\t[^\0]+\0" head)
+ (string= (car (split-string sha1 "\n")) (match-string 1 head)))))
+
+(defun vc-git-register (file &optional rev comment)
+ "Register FILE into the git version-control system."
+ (vc-git--run-command file "update-index" "--add" "--"))
+
+(defun vc-git-print-log (file &optional buffer)
+ (let ((name (file-relative-name file))
+ (coding-system-for-read git-commits-coding-system))
+ (vc-do-command buffer 'async "git" name "rev-list" "--pretty" "HEAD" "--")))
+
+(defun vc-git-diff (file &optional rev1 rev2 buffer)
+ (let ((name (file-relative-name file))
+ (buf (or buffer "*vc-diff*")))
+ (if (and rev1 rev2)
+ (vc-do-command buf 0 "git" name "diff-tree" "-p" rev1 rev2 "--")
+ (vc-do-command buf 0 "git" name "diff-index" "-p" (or rev1 "HEAD") "--"))
+ ; git-diff-index doesn't set exit status like diff does
+ (if (vc-git-workfile-unchanged-p file) 0 1)))
+
+(defun vc-git-checkin (file rev comment)
+ (let ((coding-system-for-write git-commits-coding-system))
+ (vc-git--run-command file "commit" "-m" comment "--only" "--")))
+
+(defun vc-git-checkout (file &optional editable rev destfile)
+ (if destfile
+ (let ((fullname (substring
+ (vc-git--run-command-string file "ls-files" "-z" "--full-name" "--")
+ 0 -1))
+ (coding-system-for-read 'no-conversion)
+ (coding-system-for-write 'no-conversion))
+ (with-temp-file destfile
+ (eq 0 (call-process "git" nil t nil "cat-file" "blob"
+ (concat (or rev "HEAD") ":" fullname)))))
+ (vc-git--run-command file "checkout" (or rev "HEAD"))))
+
+(defun vc-git-annotate-command (file buf &optional rev)
+ ; FIXME: rev is ignored
+ (let ((name (file-relative-name file)))
+ (call-process "git" nil buf nil "blame" name)))
+
+(defun vc-git-annotate-time ()
+ (and (re-search-forward "[0-9a-f]+ (.* \\([0-9]+\\)-\\([0-9]+\\)-\\([0-9]+\\) \\([0-9]+\\):\\([0-9]+\\):\\([0-9]+\\) \\([-+0-9]+\\) +[0-9]+)" nil t)
+ (vc-annotate-convert-time
+ (apply #'encode-time (mapcar (lambda (match) (string-to-number (match-string match))) '(6 5 4 3 2 1 7))))))
+
+;; Not really useful since we can't do anything with the revision yet
+;;(defun vc-annotate-extract-revision-at-line ()
+;; (save-excursion
+;; (move-beginning-of-line 1)
+;; (and (looking-at "[0-9a-f]+")
+;; (buffer-substring (match-beginning 0) (match-end 0)))))
+
+(provide 'vc-git)
diff --git a/contrib/examples/git-gc.sh b/contrib/examples/git-gc.sh
new file mode 100755
index 0000000000..436d7caff5
--- /dev/null
+++ b/contrib/examples/git-gc.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+#
+# Copyright (c) 2006, Shawn O. Pearce
+#
+# Cleanup unreachable files and optimize the repository.
+
+USAGE='[--prune]'
+SUBDIRECTORY_OK=Yes
+. git-sh-setup
+
+no_prune=:
+while case $# in 0) break ;; esac
+do
+ case "$1" in
+ --prune)
+ no_prune=
+ ;;
+ --)
+ usage
+ ;;
+ esac
+ shift
+done
+
+case "$(git config --get gc.packrefs)" in
+notbare|"")
+ test $(is_bare_repository) = true || pack_refs=true;;
+*)
+ pack_refs=$(git config --bool --get gc.packrefs)
+esac
+
+test "true" != "$pack_refs" ||
+git-pack-refs --prune &&
+git-reflog expire --all &&
+git-repack -a -d -l &&
+$no_prune git-prune &&
+git-rerere gc || exit
diff --git a/contrib/examples/git-resolve.sh b/contrib/examples/git-resolve.sh
new file mode 100755
index 0000000000..36b90e3849
--- /dev/null
+++ b/contrib/examples/git-resolve.sh
@@ -0,0 +1,112 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Linus Torvalds
+#
+# Resolve two trees.
+#
+
+echo 'WARNING: This command is DEPRECATED and will be removed very soon.' >&2
+echo 'WARNING: Please use git-merge or git-pull instead.' >&2
+sleep 2
+
+USAGE='<head> <remote> <merge-message>'
+. git-sh-setup
+
+dropheads() {
+ rm -f -- "$GIT_DIR/MERGE_HEAD" \
+ "$GIT_DIR/LAST_MERGE" || exit 1
+}
+
+head=$(git-rev-parse --verify "$1"^0) &&
+merge=$(git-rev-parse --verify "$2"^0) &&
+merge_name="$2" &&
+merge_msg="$3" || usage
+
+#
+# The remote name is just used for the message,
+# but we do want it.
+#
+if [ -z "$head" -o -z "$merge" -o -z "$merge_msg" ]; then
+ usage
+fi
+
+dropheads
+echo $head > "$GIT_DIR"/ORIG_HEAD
+echo $merge > "$GIT_DIR"/LAST_MERGE
+
+common=$(git-merge-base $head $merge)
+if [ -z "$common" ]; then
+ die "Unable to find common commit between" $merge $head
+fi
+
+case "$common" in
+"$merge")
+ echo "Already up-to-date. Yeeah!"
+ dropheads
+ exit 0
+ ;;
+"$head")
+ echo "Updating $(git-rev-parse --short $head)..$(git-rev-parse --short $merge)"
+ git-read-tree -u -m $head $merge || exit 1
+ git-update-ref -m "resolve $merge_name: Fast forward" \
+ HEAD "$merge" "$head"
+ git-diff-tree -p $head $merge | git-apply --stat
+ dropheads
+ exit 0
+ ;;
+esac
+
+# We are going to make a new commit.
+git var GIT_COMMITTER_IDENT >/dev/null || exit
+
+# Find an optimum merge base if there are more than one candidates.
+LF='
+'
+common=$(git-merge-base -a $head $merge)
+case "$common" in
+?*"$LF"?*)
+ echo "Trying to find the optimum merge base."
+ G=.tmp-index$$
+ best=
+ best_cnt=-1
+ for c in $common
+ do
+ rm -f $G
+ GIT_INDEX_FILE=$G git-read-tree -m $c $head $merge \
+ 2>/dev/null || continue
+ # Count the paths that are unmerged.
+ cnt=`GIT_INDEX_FILE=$G git-ls-files --unmerged | wc -l`
+ if test $best_cnt -le 0 -o $cnt -le $best_cnt
+ then
+ best=$c
+ best_cnt=$cnt
+ if test "$best_cnt" -eq 0
+ then
+ # Cannot do any better than all trivial merge.
+ break
+ fi
+ fi
+ done
+ rm -f $G
+ common="$best"
+esac
+
+echo "Trying to merge $merge into $head using $common."
+git-update-index --refresh 2>/dev/null
+git-read-tree -u -m $common $head $merge || exit 1
+result_tree=$(git-write-tree 2> /dev/null)
+if [ $? -ne 0 ]; then
+ echo "Simple merge failed, trying Automatic merge"
+ git-merge-index -o git-merge-one-file -a
+ if [ $? -ne 0 ]; then
+ echo $merge > "$GIT_DIR"/MERGE_HEAD
+ die "Automatic merge failed, fix up by hand"
+ fi
+ result_tree=$(git-write-tree) || exit 1
+fi
+result_commit=$(echo "$merge_msg" | git-commit-tree $result_tree -p $head -p $merge)
+echo "Committed merge $result_commit"
+git-update-ref -m "resolve $merge_name: In-index merge" \
+ HEAD "$result_commit" "$head"
+git-diff-tree -p $head $result_commit | git-apply --stat
+dropheads
diff --git a/contrib/fast-import/import-tars.perl b/contrib/fast-import/import-tars.perl
new file mode 100755
index 0000000000..e46492048c
--- /dev/null
+++ b/contrib/fast-import/import-tars.perl
@@ -0,0 +1,126 @@
+#!/usr/bin/perl
+
+## tar archive frontend for git-fast-import
+##
+## For example:
+##
+## mkdir project; cd project; git init
+## perl import-tars.perl *.tar.bz2
+## git whatchanged import-tars
+##
+
+use strict;
+die "usage: import-tars *.tar.{gz,bz2,Z}\n" unless @ARGV;
+
+my $branch_name = 'import-tars';
+my $branch_ref = "refs/heads/$branch_name";
+my $committer_name = 'T Ar Creator';
+my $committer_email = 'tar@example.com';
+
+open(FI, '|-', 'git', 'fast-import', '--quiet')
+ or die "Unable to start git fast-import: $!\n";
+foreach my $tar_file (@ARGV)
+{
+ $tar_file =~ m,([^/]+)$,;
+ my $tar_name = $1;
+
+ if ($tar_name =~ s/\.(tar\.gz|tgz)$//) {
+ open(I, '-|', 'gunzip', '-c', $tar_file)
+ or die "Unable to gunzip -c $tar_file: $!\n";
+ } elsif ($tar_name =~ s/\.(tar\.bz2|tbz2)$//) {
+ open(I, '-|', 'bunzip2', '-c', $tar_file)
+ or die "Unable to bunzip2 -c $tar_file: $!\n";
+ } elsif ($tar_name =~ s/\.tar\.Z$//) {
+ open(I, '-|', 'uncompress', '-c', $tar_file)
+ or die "Unable to uncompress -c $tar_file: $!\n";
+ } elsif ($tar_name =~ s/\.tar$//) {
+ open(I, $tar_file) or die "Unable to open $tar_file: $!\n";
+ } else {
+ die "Unrecognized compression format: $tar_file\n";
+ }
+
+ my $commit_time = 0;
+ my $next_mark = 1;
+ my $have_top_dir = 1;
+ my ($top_dir, %files);
+
+ while (read(I, $_, 512) == 512) {
+ my ($name, $mode, $uid, $gid, $size, $mtime,
+ $chksum, $typeflag, $linkname, $magic,
+ $version, $uname, $gname, $devmajor, $devminor,
+ $prefix) = unpack 'Z100 Z8 Z8 Z8 Z12 Z12
+ Z8 Z1 Z100 Z6
+ Z2 Z32 Z32 Z8 Z8 Z*', $_;
+ last unless $name;
+ if ($name eq '././@LongLink') {
+ # GNU tar extension
+ if (read(I, $_, 512) != 512) {
+ die ('Short archive');
+ }
+ $name = unpack 'Z257', $_;
+ next unless $name;
+
+ my $dummy;
+ if (read(I, $_, 512) != 512) {
+ die ('Short archive');
+ }
+ ($dummy, $mode, $uid, $gid, $size, $mtime,
+ $chksum, $typeflag, $linkname, $magic,
+ $version, $uname, $gname, $devmajor, $devminor,
+ $prefix) = unpack 'Z100 Z8 Z8 Z8 Z12 Z12
+ Z8 Z1 Z100 Z6
+ Z2 Z32 Z32 Z8 Z8 Z*', $_;
+ }
+ next if $name =~ m{/\z};
+ $mode = oct $mode;
+ $size = oct $size;
+ $mtime = oct $mtime;
+ next if $mode & 0040000;
+
+ print FI "blob\n", "mark :$next_mark\n", "data $size\n";
+ while ($size > 0 && read(I, $_, 512) == 512) {
+ print FI substr($_, 0, $size);
+ $size -= 512;
+ }
+ print FI "\n";
+
+ my $path = "$prefix$name";
+ $files{$path} = [$next_mark++, $mode];
+
+ $commit_time = $mtime if $mtime > $commit_time;
+ $path =~ m,^([^/]+)/,;
+ $top_dir = $1 unless $top_dir;
+ $have_top_dir = 0 if $top_dir ne $1;
+ }
+
+ print FI <<EOF;
+commit $branch_ref
+committer $committer_name <$committer_email> $commit_time +0000
+data <<END_OF_COMMIT_MESSAGE
+Imported from $tar_file.
+END_OF_COMMIT_MESSAGE
+
+deleteall
+EOF
+
+ foreach my $path (keys %files)
+ {
+ my ($mark, $mode) = @{$files{$path}};
+ $path =~ s,^([^/]+)/,, if $have_top_dir;
+ printf FI "M %o :%i %s\n", $mode & 0111 ? 0755 : 0644, $mark, $path;
+ }
+ print FI "\n";
+
+ print FI <<EOF;
+tag $tar_name
+from $branch_ref
+tagger $committer_name <$committer_email> $commit_time +0000
+data <<END_OF_TAG_MESSAGE
+Package $tar_name
+END_OF_TAG_MESSAGE
+
+EOF
+
+ close I;
+}
+close FI;
diff --git a/contrib/gitview/gitview b/contrib/gitview/gitview
new file mode 100755
index 0000000000..521b2fcd32
--- /dev/null
+++ b/contrib/gitview/gitview
@@ -0,0 +1,1029 @@
+#! /usr/bin/env python
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+""" gitview
+GUI browser for git repository
+This program is based on bzrk by Scott James Remnant <scott@ubuntu.com>
+"""
+__copyright__ = "Copyright (C) 2006 Hewlett-Packard Development Company, L.P."
+__author__ = "Aneesh Kumar K.V <aneesh.kumar@hp.com>"
+
+
+import sys
+import os
+import gtk
+import pygtk
+import pango
+import re
+import time
+import gobject
+import cairo
+import math
+import string
+
+try:
+ import gtksourceview
+ have_gtksourceview = True
+except ImportError:
+ have_gtksourceview = False
+ print "Running without gtksourceview module"
+
+re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
+
+def list_to_string(args, skip):
+ count = len(args)
+ i = skip
+ str_arg=" "
+ while (i < count ):
+ str_arg = str_arg + args[i]
+ str_arg = str_arg + " "
+ i = i+1
+
+ return str_arg
+
+def show_date(epoch, tz):
+ secs = float(epoch)
+ tzsecs = float(tz[1:3]) * 3600
+ tzsecs += float(tz[3:5]) * 60
+ if (tz[0] == "+"):
+ secs += tzsecs
+ else:
+ secs -= tzsecs
+
+ return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
+
+
+class CellRendererGraph(gtk.GenericCellRenderer):
+ """Cell renderer for directed graph.
+
+ This module contains the implementation of a custom GtkCellRenderer that
+ draws part of the directed graph based on the lines suggested by the code
+ in graph.py.
+
+ Because we're shiny, we use Cairo to do this, and because we're naughty
+ we cheat and draw over the bits of the TreeViewColumn that are supposed to
+ just be for the background.
+
+ Properties:
+ node (column, colour, [ names ]) tuple to draw revision node,
+ in_lines (start, end, colour) tuple list to draw inward lines,
+ out_lines (start, end, colour) tuple list to draw outward lines.
+ """
+
+ __gproperties__ = {
+ "node": ( gobject.TYPE_PYOBJECT, "node",
+ "revision node instruction",
+ gobject.PARAM_WRITABLE
+ ),
+ "in-lines": ( gobject.TYPE_PYOBJECT, "in-lines",
+ "instructions to draw lines into the cell",
+ gobject.PARAM_WRITABLE
+ ),
+ "out-lines": ( gobject.TYPE_PYOBJECT, "out-lines",
+ "instructions to draw lines out of the cell",
+ gobject.PARAM_WRITABLE
+ ),
+ }
+
+ def do_set_property(self, property, value):
+ """Set properties from GObject properties."""
+ if property.name == "node":
+ self.node = value
+ elif property.name == "in-lines":
+ self.in_lines = value
+ elif property.name == "out-lines":
+ self.out_lines = value
+ else:
+ raise AttributeError, "no such property: '%s'" % property.name
+
+ def box_size(self, widget):
+ """Calculate box size based on widget's font.
+
+ Cache this as it's probably expensive to get. It ensures that we
+ draw the graph at least as large as the text.
+ """
+ try:
+ return self._box_size
+ except AttributeError:
+ pango_ctx = widget.get_pango_context()
+ font_desc = widget.get_style().font_desc
+ metrics = pango_ctx.get_metrics(font_desc)
+
+ ascent = pango.PIXELS(metrics.get_ascent())
+ descent = pango.PIXELS(metrics.get_descent())
+
+ self._box_size = ascent + descent + 6
+ return self._box_size
+
+ def set_colour(self, ctx, colour, bg, fg):
+ """Set the context source colour.
+
+ Picks a distinct colour based on an internal wheel; the bg
+ parameter provides the value that should be assigned to the 'zero'
+ colours and the fg parameter provides the multiplier that should be
+ applied to the foreground colours.
+ """
+ colours = [
+ ( 1.0, 0.0, 0.0 ),
+ ( 1.0, 1.0, 0.0 ),
+ ( 0.0, 1.0, 0.0 ),
+ ( 0.0, 1.0, 1.0 ),
+ ( 0.0, 0.0, 1.0 ),
+ ( 1.0, 0.0, 1.0 ),
+ ]
+
+ colour %= len(colours)
+ red = (colours[colour][0] * fg) or bg
+ green = (colours[colour][1] * fg) or bg
+ blue = (colours[colour][2] * fg) or bg
+
+ ctx.set_source_rgb(red, green, blue)
+
+ def on_get_size(self, widget, cell_area):
+ """Return the size we need for this cell.
+
+ Each cell is drawn individually and is only as wide as it needs
+ to be, we let the TreeViewColumn take care of making them all
+ line up.
+ """
+ box_size = self.box_size(widget)
+
+ cols = self.node[0]
+ for start, end, colour in self.in_lines + self.out_lines:
+ cols = int(max(cols, start, end))
+
+ (column, colour, names) = self.node
+ names_len = 0
+ if (len(names) != 0):
+ for item in names:
+ names_len += len(item)
+
+ width = box_size * (cols + 1 ) + names_len
+ height = box_size
+
+ # FIXME I have no idea how to use cell_area properly
+ return (0, 0, width, height)
+
+ def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
+ """Render an individual cell.
+
+ Draws the cell contents using cairo, taking care to clip what we
+ do to within the background area so we don't draw over other cells.
+ Note that we're a bit naughty there and should really be drawing
+ in the cell_area (or even the exposed area), but we explicitly don't
+ want any gutter.
+
+ We try and be a little clever, if the line we need to draw is going
+ to cross other columns we actually draw it as in the .---' style
+ instead of a pure diagonal ... this reduces confusion by an
+ incredible amount.
+ """
+ ctx = window.cairo_create()
+ ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
+ ctx.clip()
+
+ box_size = self.box_size(widget)
+
+ ctx.set_line_width(box_size / 8)
+ ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
+
+ # Draw lines into the cell
+ for start, end, colour in self.in_lines:
+ ctx.move_to(cell_area.x + box_size * start + box_size / 2,
+ bg_area.y - bg_area.height / 2)
+
+ if start - end > 1:
+ ctx.line_to(cell_area.x + box_size * start, bg_area.y)
+ ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y)
+ elif start - end < -1:
+ ctx.line_to(cell_area.x + box_size * start + box_size,
+ bg_area.y)
+ ctx.line_to(cell_area.x + box_size * end, bg_area.y)
+
+ ctx.line_to(cell_area.x + box_size * end + box_size / 2,
+ bg_area.y + bg_area.height / 2)
+
+ self.set_colour(ctx, colour, 0.0, 0.65)
+ ctx.stroke()
+
+ # Draw lines out of the cell
+ for start, end, colour in self.out_lines:
+ ctx.move_to(cell_area.x + box_size * start + box_size / 2,
+ bg_area.y + bg_area.height / 2)
+
+ if start - end > 1:
+ ctx.line_to(cell_area.x + box_size * start,
+ bg_area.y + bg_area.height)
+ ctx.line_to(cell_area.x + box_size * end + box_size,
+ bg_area.y + bg_area.height)
+ elif start - end < -1:
+ ctx.line_to(cell_area.x + box_size * start + box_size,
+ bg_area.y + bg_area.height)
+ ctx.line_to(cell_area.x + box_size * end,
+ bg_area.y + bg_area.height)
+
+ ctx.line_to(cell_area.x + box_size * end + box_size / 2,
+ bg_area.y + bg_area.height / 2 + bg_area.height)
+
+ self.set_colour(ctx, colour, 0.0, 0.65)
+ ctx.stroke()
+
+ # Draw the revision node in the right column
+ (column, colour, names) = self.node
+ ctx.arc(cell_area.x + box_size * column + box_size / 2,
+ cell_area.y + cell_area.height / 2,
+ box_size / 4, 0, 2 * math.pi)
+
+
+ self.set_colour(ctx, colour, 0.0, 0.5)
+ ctx.stroke_preserve()
+
+ self.set_colour(ctx, colour, 0.5, 1.0)
+ ctx.fill_preserve()
+
+ if (len(names) != 0):
+ name = " "
+ for item in names:
+ name = name + item + " "
+
+ ctx.set_font_size(13)
+ if (flags & 1):
+ self.set_colour(ctx, colour, 0.5, 1.0)
+ else:
+ self.set_colour(ctx, colour, 0.0, 0.5)
+ ctx.show_text(name)
+
+class Commit:
+ """ This represent a commit object obtained after parsing the git-rev-list
+ output """
+
+ children_sha1 = {}
+
+ def __init__(self, commit_lines):
+ self.message = ""
+ self.author = ""
+ self.date = ""
+ self.committer = ""
+ self.commit_date = ""
+ self.commit_sha1 = ""
+ self.parent_sha1 = [ ]
+ self.parse_commit(commit_lines)
+
+
+ def parse_commit(self, commit_lines):
+
+ # First line is the sha1 lines
+ line = string.strip(commit_lines[0])
+ sha1 = re.split(" ", line)
+ self.commit_sha1 = sha1[0]
+ self.parent_sha1 = sha1[1:]
+
+ #build the child list
+ for parent_id in self.parent_sha1:
+ try:
+ Commit.children_sha1[parent_id].append(self.commit_sha1)
+ except KeyError:
+ Commit.children_sha1[parent_id] = [self.commit_sha1]
+
+ # IF we don't have parent
+ if (len(self.parent_sha1) == 0):
+ self.parent_sha1 = [0]
+
+ for line in commit_lines[1:]:
+ m = re.match("^ ", line)
+ if (m != None):
+ # First line of the commit message used for short log
+ if self.message == "":
+ self.message = string.strip(line)
+ continue
+
+ m = re.match("tree", line)
+ if (m != None):
+ continue
+
+ m = re.match("parent", line)
+ if (m != None):
+ continue
+
+ m = re_ident.match(line)
+ if (m != None):
+ date = show_date(m.group('epoch'), m.group('tz'))
+ if m.group(1) == "author":
+ self.author = m.group('ident')
+ self.date = date
+ elif m.group(1) == "committer":
+ self.committer = m.group('ident')
+ self.commit_date = date
+
+ continue
+
+ def get_message(self, with_diff=0):
+ if (with_diff == 1):
+ message = self.diff_tree()
+ else:
+ fp = os.popen("git cat-file commit " + self.commit_sha1)
+ message = fp.read()
+ fp.close()
+
+ return message
+
+ def diff_tree(self):
+ fp = os.popen("git diff-tree --pretty --cc -v -p --always " + self.commit_sha1)
+ diff = fp.read()
+ fp.close()
+ return diff
+
+class DiffWindow:
+ """Diff window.
+ This object represents and manages a single window containing the
+ differences between two revisions on a branch.
+ """
+
+ def __init__(self):
+ self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ self.window.set_border_width(0)
+ self.window.set_title("Git repository browser diff window")
+
+ # Use two thirds of the screen by default
+ screen = self.window.get_screen()
+ monitor = screen.get_monitor_geometry(0)
+ width = int(monitor.width * 0.66)
+ height = int(monitor.height * 0.66)
+ self.window.set_default_size(width, height)
+
+ self.construct()
+
+ def construct(self):
+ """Construct the window contents."""
+ vbox = gtk.VBox()
+ self.window.add(vbox)
+ vbox.show()
+
+ menu_bar = gtk.MenuBar()
+ save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
+ save_menu.connect("activate", self.save_menu_response, "save")
+ save_menu.show()
+ menu_bar.append(save_menu)
+ vbox.pack_start(menu_bar, expand=False, fill=True)
+ menu_bar.show()
+
+ scrollwin = gtk.ScrolledWindow()
+ scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ scrollwin.set_shadow_type(gtk.SHADOW_IN)
+ vbox.pack_start(scrollwin, expand=True, fill=True)
+ scrollwin.show()
+
+ if have_gtksourceview:
+ self.buffer = gtksourceview.SourceBuffer()
+ slm = gtksourceview.SourceLanguagesManager()
+ gsl = slm.get_language_from_mime_type("text/x-patch")
+ self.buffer.set_highlight(True)
+ self.buffer.set_language(gsl)
+ sourceview = gtksourceview.SourceView(self.buffer)
+ else:
+ self.buffer = gtk.TextBuffer()
+ sourceview = gtk.TextView(self.buffer)
+
+ sourceview.set_editable(False)
+ sourceview.modify_font(pango.FontDescription("Monospace"))
+ scrollwin.add(sourceview)
+ sourceview.show()
+
+
+ def set_diff(self, commit_sha1, parent_sha1, encoding):
+ """Set the differences showed by this window.
+ Compares the two trees and populates the window with the
+ differences.
+ """
+ # Diff with the first commit or the last commit shows nothing
+ if (commit_sha1 == 0 or parent_sha1 == 0 ):
+ return
+
+ fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
+ self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8'))
+ fp.close()
+ self.window.show()
+
+ def save_menu_response(self, widget, string):
+ dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
+ (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+ gtk.STOCK_SAVE, gtk.RESPONSE_OK))
+ dialog.set_default_response(gtk.RESPONSE_OK)
+ response = dialog.run()
+ if response == gtk.RESPONSE_OK:
+ patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
+ self.buffer.get_end_iter())
+ fp = open(dialog.get_filename(), "w")
+ fp.write(patch_buffer)
+ fp.close()
+ dialog.destroy()
+
+class GitView:
+ """ This is the main class
+ """
+ version = "0.8"
+
+ def __init__(self, with_diff=0):
+ self.with_diff = with_diff
+ self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ self.window.set_border_width(0)
+ self.window.set_title("Git repository browser")
+
+ self.get_encoding()
+ self.get_bt_sha1()
+
+ # Use three-quarters of the screen by default
+ screen = self.window.get_screen()
+ monitor = screen.get_monitor_geometry(0)
+ width = int(monitor.width * 0.75)
+ height = int(monitor.height * 0.75)
+ self.window.set_default_size(width, height)
+
+ # FIXME AndyFitz!
+ icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
+ self.window.set_icon(icon)
+
+ self.accel_group = gtk.AccelGroup()
+ self.window.add_accel_group(self.accel_group)
+ self.accel_group.connect_group(0xffc2, 0, gtk.ACCEL_LOCKED, self.refresh);
+ self.accel_group.connect_group(0xffc1, 0, gtk.ACCEL_LOCKED, self.maximize);
+ self.accel_group.connect_group(0xffc8, 0, gtk.ACCEL_LOCKED, self.fullscreen);
+ self.accel_group.connect_group(0xffc9, 0, gtk.ACCEL_LOCKED, self.unfullscreen);
+
+ self.window.add(self.construct())
+
+ def refresh(self, widget, event=None, *arguments, **keywords):
+ self.get_encoding()
+ self.get_bt_sha1()
+ Commit.children_sha1 = {}
+ self.set_branch(sys.argv[without_diff:])
+ self.window.show()
+ return True
+
+ def maximize(self, widget, event=None, *arguments, **keywords):
+ self.window.maximize()
+ return True
+
+ def fullscreen(self, widget, event=None, *arguments, **keywords):
+ self.window.fullscreen()
+ return True
+
+ def unfullscreen(self, widget, event=None, *arguments, **keywords):
+ self.window.unfullscreen()
+ return True
+
+ def get_bt_sha1(self):
+ """ Update the bt_sha1 dictionary with the
+ respective sha1 details """
+
+ self.bt_sha1 = { }
+ ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$');
+ fp = os.popen('git ls-remote "${GIT_DIR-.git}"')
+ while 1:
+ line = string.strip(fp.readline())
+ if line == '':
+ break
+ m = ls_remote.match(line)
+ if not m:
+ continue
+ (sha1, name) = (m.group(1), m.group(2))
+ if not self.bt_sha1.has_key(sha1):
+ self.bt_sha1[sha1] = []
+ self.bt_sha1[sha1].append(name)
+ fp.close()
+
+ def get_encoding(self):
+ fp = os.popen("git config --get i18n.commitencoding")
+ self.encoding=string.strip(fp.readline())
+ fp.close()
+ if (self.encoding == ""):
+ self.encoding = "utf-8"
+
+
+ def construct(self):
+ """Construct the window contents."""
+ vbox = gtk.VBox()
+ paned = gtk.VPaned()
+ paned.pack1(self.construct_top(), resize=False, shrink=True)
+ paned.pack2(self.construct_bottom(), resize=False, shrink=True)
+ menu_bar = gtk.MenuBar()
+ menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
+ help_menu = gtk.MenuItem("Help")
+ menu = gtk.Menu()
+ about_menu = gtk.MenuItem("About")
+ menu.append(about_menu)
+ about_menu.connect("activate", self.about_menu_response, "about")
+ about_menu.show()
+ help_menu.set_submenu(menu)
+ help_menu.show()
+ menu_bar.append(help_menu)
+ menu_bar.show()
+ vbox.pack_start(menu_bar, expand=False, fill=True)
+ vbox.pack_start(paned, expand=True, fill=True)
+ paned.show()
+ vbox.show()
+ return vbox
+
+
+ def construct_top(self):
+ """Construct the top-half of the window."""
+ vbox = gtk.VBox(spacing=6)
+ vbox.set_border_width(12)
+ vbox.show()
+
+
+ scrollwin = gtk.ScrolledWindow()
+ scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ scrollwin.set_shadow_type(gtk.SHADOW_IN)
+ vbox.pack_start(scrollwin, expand=True, fill=True)
+ scrollwin.show()
+
+ self.treeview = gtk.TreeView()
+ self.treeview.set_rules_hint(True)
+ self.treeview.set_search_column(4)
+ self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
+ scrollwin.add(self.treeview)
+ self.treeview.show()
+
+ cell = CellRendererGraph()
+ column = gtk.TreeViewColumn()
+ column.set_resizable(True)
+ column.pack_start(cell, expand=True)
+ column.add_attribute(cell, "node", 1)
+ column.add_attribute(cell, "in-lines", 2)
+ column.add_attribute(cell, "out-lines", 3)
+ self.treeview.append_column(column)
+
+ cell = gtk.CellRendererText()
+ cell.set_property("width-chars", 65)
+ cell.set_property("ellipsize", pango.ELLIPSIZE_END)
+ column = gtk.TreeViewColumn("Message")
+ column.set_resizable(True)
+ column.pack_start(cell, expand=True)
+ column.add_attribute(cell, "text", 4)
+ self.treeview.append_column(column)
+
+ cell = gtk.CellRendererText()
+ cell.set_property("width-chars", 40)
+ cell.set_property("ellipsize", pango.ELLIPSIZE_END)
+ column = gtk.TreeViewColumn("Author")
+ column.set_resizable(True)
+ column.pack_start(cell, expand=True)
+ column.add_attribute(cell, "text", 5)
+ self.treeview.append_column(column)
+
+ cell = gtk.CellRendererText()
+ cell.set_property("ellipsize", pango.ELLIPSIZE_END)
+ column = gtk.TreeViewColumn("Date")
+ column.set_resizable(True)
+ column.pack_start(cell, expand=True)
+ column.add_attribute(cell, "text", 6)
+ self.treeview.append_column(column)
+
+ return vbox
+
+ def about_menu_response(self, widget, string):
+ dialog = gtk.AboutDialog()
+ dialog.set_name("Gitview")
+ dialog.set_version(GitView.version)
+ dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
+ dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
+ dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
+ dialog.set_wrap_license(True)
+ dialog.run()
+ dialog.destroy()
+
+
+ def construct_bottom(self):
+ """Construct the bottom half of the window."""
+ vbox = gtk.VBox(False, spacing=6)
+ vbox.set_border_width(12)
+ (width, height) = self.window.get_size()
+ vbox.set_size_request(width, int(height / 2.5))
+ vbox.show()
+
+ self.table = gtk.Table(rows=4, columns=4)
+ self.table.set_row_spacings(6)
+ self.table.set_col_spacings(6)
+ vbox.pack_start(self.table, expand=False, fill=True)
+ self.table.show()
+
+ align = gtk.Alignment(0.0, 0.5)
+ label = gtk.Label()
+ label.set_markup("<b>Revision:</b>")
+ align.add(label)
+ self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
+ label.show()
+ align.show()
+
+ align = gtk.Alignment(0.0, 0.5)
+ self.revid_label = gtk.Label()
+ self.revid_label.set_selectable(True)
+ align.add(self.revid_label)
+ self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
+ self.revid_label.show()
+ align.show()
+
+ align = gtk.Alignment(0.0, 0.5)
+ label = gtk.Label()
+ label.set_markup("<b>Committer:</b>")
+ align.add(label)
+ self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
+ label.show()
+ align.show()
+
+ align = gtk.Alignment(0.0, 0.5)
+ self.committer_label = gtk.Label()
+ self.committer_label.set_selectable(True)
+ align.add(self.committer_label)
+ self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
+ self.committer_label.show()
+ align.show()
+
+ align = gtk.Alignment(0.0, 0.5)
+ label = gtk.Label()
+ label.set_markup("<b>Timestamp:</b>")
+ align.add(label)
+ self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
+ label.show()
+ align.show()
+
+ align = gtk.Alignment(0.0, 0.5)
+ self.timestamp_label = gtk.Label()
+ self.timestamp_label.set_selectable(True)
+ align.add(self.timestamp_label)
+ self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
+ self.timestamp_label.show()
+ align.show()
+
+ align = gtk.Alignment(0.0, 0.5)
+ label = gtk.Label()
+ label.set_markup("<b>Parents:</b>")
+ align.add(label)
+ self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
+ label.show()
+ align.show()
+ self.parents_widgets = []
+
+ align = gtk.Alignment(0.0, 0.5)
+ label = gtk.Label()
+ label.set_markup("<b>Children:</b>")
+ align.add(label)
+ self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
+ label.show()
+ align.show()
+ self.children_widgets = []
+
+ scrollwin = gtk.ScrolledWindow()
+ scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ scrollwin.set_shadow_type(gtk.SHADOW_IN)
+ vbox.pack_start(scrollwin, expand=True, fill=True)
+ scrollwin.show()
+
+ if have_gtksourceview:
+ self.message_buffer = gtksourceview.SourceBuffer()
+ slm = gtksourceview.SourceLanguagesManager()
+ gsl = slm.get_language_from_mime_type("text/x-patch")
+ self.message_buffer.set_highlight(True)
+ self.message_buffer.set_language(gsl)
+ sourceview = gtksourceview.SourceView(self.message_buffer)
+ else:
+ self.message_buffer = gtk.TextBuffer()
+ sourceview = gtk.TextView(self.message_buffer)
+
+ sourceview.set_editable(False)
+ sourceview.modify_font(pango.FontDescription("Monospace"))
+ scrollwin.add(sourceview)
+ sourceview.show()
+
+ return vbox
+
+ def _treeview_cursor_cb(self, *args):
+ """Callback for when the treeview cursor changes."""
+ (path, col) = self.treeview.get_cursor()
+ commit = self.model[path][0]
+
+ if commit.committer is not None:
+ committer = commit.committer
+ timestamp = commit.commit_date
+ message = commit.get_message(self.with_diff)
+ revid_label = commit.commit_sha1
+ else:
+ committer = ""
+ timestamp = ""
+ message = ""
+ revid_label = ""
+
+ self.revid_label.set_text(revid_label)
+ self.committer_label.set_text(committer)
+ self.timestamp_label.set_text(timestamp)
+ self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8'))
+
+ for widget in self.parents_widgets:
+ self.table.remove(widget)
+
+ self.parents_widgets = []
+ self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
+ for idx, parent_id in enumerate(commit.parent_sha1):
+ self.table.set_row_spacing(idx + 3, 0)
+
+ align = gtk.Alignment(0.0, 0.0)
+ self.parents_widgets.append(align)
+ self.table.attach(align, 1, 2, idx + 3, idx + 4,
+ gtk.EXPAND | gtk.FILL, gtk.FILL)
+ align.show()
+
+ hbox = gtk.HBox(False, 0)
+ align.add(hbox)
+ hbox.show()
+
+ label = gtk.Label(parent_id)
+ label.set_selectable(True)
+ hbox.pack_start(label, expand=False, fill=True)
+ label.show()
+
+ image = gtk.Image()
+ image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
+ image.show()
+
+ button = gtk.Button()
+ button.add(image)
+ button.set_relief(gtk.RELIEF_NONE)
+ button.connect("clicked", self._go_clicked_cb, parent_id)
+ hbox.pack_start(button, expand=False, fill=True)
+ button.show()
+
+ image = gtk.Image()
+ image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
+ image.show()
+
+ button = gtk.Button()
+ button.add(image)
+ button.set_relief(gtk.RELIEF_NONE)
+ button.set_sensitive(True)
+ button.connect("clicked", self._show_clicked_cb,
+ commit.commit_sha1, parent_id, self.encoding)
+ hbox.pack_start(button, expand=False, fill=True)
+ button.show()
+
+ # Populate with child details
+ for widget in self.children_widgets:
+ self.table.remove(widget)
+
+ self.children_widgets = []
+ try:
+ child_sha1 = Commit.children_sha1[commit.commit_sha1]
+ except KeyError:
+ # We don't have child
+ child_sha1 = [ 0 ]
+
+ if ( len(child_sha1) > len(commit.parent_sha1)):
+ self.table.resize(4 + len(child_sha1) - 1, 4)
+
+ for idx, child_id in enumerate(child_sha1):
+ self.table.set_row_spacing(idx + 3, 0)
+
+ align = gtk.Alignment(0.0, 0.0)
+ self.children_widgets.append(align)
+ self.table.attach(align, 3, 4, idx + 3, idx + 4,
+ gtk.EXPAND | gtk.FILL, gtk.FILL)
+ align.show()
+
+ hbox = gtk.HBox(False, 0)
+ align.add(hbox)
+ hbox.show()
+
+ label = gtk.Label(child_id)
+ label.set_selectable(True)
+ hbox.pack_start(label, expand=False, fill=True)
+ label.show()
+
+ image = gtk.Image()
+ image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
+ image.show()
+
+ button = gtk.Button()
+ button.add(image)
+ button.set_relief(gtk.RELIEF_NONE)
+ button.connect("clicked", self._go_clicked_cb, child_id)
+ hbox.pack_start(button, expand=False, fill=True)
+ button.show()
+
+ image = gtk.Image()
+ image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
+ image.show()
+
+ button = gtk.Button()
+ button.add(image)
+ button.set_relief(gtk.RELIEF_NONE)
+ button.set_sensitive(True)
+ button.connect("clicked", self._show_clicked_cb,
+ child_id, commit.commit_sha1, self.encoding)
+ hbox.pack_start(button, expand=False, fill=True)
+ button.show()
+
+ def _destroy_cb(self, widget):
+ """Callback for when a window we manage is destroyed."""
+ self.quit()
+
+
+ def quit(self):
+ """Stop the GTK+ main loop."""
+ gtk.main_quit()
+
+ def run(self, args):
+ self.set_branch(args)
+ self.window.connect("destroy", self._destroy_cb)
+ self.window.show()
+ gtk.main()
+
+ def set_branch(self, args):
+ """Fill in different windows with info from the reposiroty"""
+ fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
+ git_rev_list_cmd = fp.read()
+ fp.close()
+ fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd)
+ self.update_window(fp)
+
+ def update_window(self, fp):
+ commit_lines = []
+
+ self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
+ gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
+
+ # used for cursor positioning
+ self.index = {}
+
+ self.colours = {}
+ self.nodepos = {}
+ self.incomplete_line = {}
+ self.commits = []
+
+ index = 0
+ last_colour = 0
+ last_nodepos = -1
+ out_line = []
+ input_line = fp.readline()
+ while (input_line != ""):
+ # The commit header ends with '\0'
+ # This NULL is immediately followed by the sha1 of the
+ # next commit
+ if (input_line[0] != '\0'):
+ commit_lines.append(input_line)
+ input_line = fp.readline()
+ continue;
+
+ commit = Commit(commit_lines)
+ if (commit != None ):
+ self.commits.append(commit)
+
+ # Skip the '\0
+ commit_lines = []
+ commit_lines.append(input_line[1:])
+ input_line = fp.readline()
+
+ fp.close()
+
+ for commit in self.commits:
+ (out_line, last_colour, last_nodepos) = self.draw_graph(commit,
+ index, out_line,
+ last_colour,
+ last_nodepos)
+ self.index[commit.commit_sha1] = index
+ index += 1
+
+ self.treeview.set_model(self.model)
+ self.treeview.show()
+
+ def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
+ in_line=[]
+
+ # | -> outline
+ # X
+ # |\ <- inline
+
+ # Reset nodepostion
+ if (last_nodepos > 5):
+ last_nodepos = -1
+
+ # Add the incomplete lines of the last cell in this
+ try:
+ colour = self.colours[commit.commit_sha1]
+ except KeyError:
+ self.colours[commit.commit_sha1] = last_colour+1
+ last_colour = self.colours[commit.commit_sha1]
+ colour = self.colours[commit.commit_sha1]
+
+ try:
+ node_pos = self.nodepos[commit.commit_sha1]
+ except KeyError:
+ self.nodepos[commit.commit_sha1] = last_nodepos+1
+ last_nodepos = self.nodepos[commit.commit_sha1]
+ node_pos = self.nodepos[commit.commit_sha1]
+
+ #The first parent always continue on the same line
+ try:
+ # check we alreay have the value
+ tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
+ except KeyError:
+ self.colours[commit.parent_sha1[0]] = colour
+ self.nodepos[commit.parent_sha1[0]] = node_pos
+
+ for sha1 in self.incomplete_line.keys():
+ if (sha1 != commit.commit_sha1):
+ self.draw_incomplete_line(sha1, node_pos,
+ out_line, in_line, index)
+ else:
+ del self.incomplete_line[sha1]
+
+
+ for parent_id in commit.parent_sha1:
+ try:
+ tmp_node_pos = self.nodepos[parent_id]
+ except KeyError:
+ self.colours[parent_id] = last_colour+1
+ last_colour = self.colours[parent_id]
+ self.nodepos[parent_id] = last_nodepos+1
+ last_nodepos = self.nodepos[parent_id]
+
+ in_line.append((node_pos, self.nodepos[parent_id],
+ self.colours[parent_id]))
+ self.add_incomplete_line(parent_id)
+
+ try:
+ branch_tag = self.bt_sha1[commit.commit_sha1]
+ except KeyError:
+ branch_tag = [ ]
+
+
+ node = (node_pos, colour, branch_tag)
+
+ self.model.append([commit, node, out_line, in_line,
+ commit.message, commit.author, commit.date])
+
+ return (in_line, last_colour, last_nodepos)
+
+ def add_incomplete_line(self, sha1):
+ try:
+ self.incomplete_line[sha1].append(self.nodepos[sha1])
+ except KeyError:
+ self.incomplete_line[sha1] = [self.nodepos[sha1]]
+
+ def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index):
+ for idx, pos in enumerate(self.incomplete_line[sha1]):
+ if(pos == node_pos):
+ #remove the straight line and add a slash
+ if ((pos, pos, self.colours[sha1]) in out_line):
+ out_line.remove((pos, pos, self.colours[sha1]))
+ out_line.append((pos, pos+0.5, self.colours[sha1]))
+ self.incomplete_line[sha1][idx] = pos = pos+0.5
+ try:
+ next_commit = self.commits[index+1]
+ if (next_commit.commit_sha1 == sha1 and pos != int(pos)):
+ # join the line back to the node point
+ # This need to be done only if we modified it
+ in_line.append((pos, pos-0.5, self.colours[sha1]))
+ continue;
+ except IndexError:
+ pass
+ in_line.append((pos, pos, self.colours[sha1]))
+
+
+ def _go_clicked_cb(self, widget, revid):
+ """Callback for when the go button for a parent is clicked."""
+ try:
+ self.treeview.set_cursor(self.index[revid])
+ except KeyError:
+ dialog = gtk.MessageDialog(parent=None, flags=0,
+ type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
+ message_format=None)
+ dialog.set_markup("Revision <b>%s</b> not present in the list" % revid)
+ # revid == 0 is the parent of the first commit
+ if (revid != 0 ):
+ dialog.format_secondary_text("Try running gitview without any options")
+ dialog.run()
+ dialog.destroy()
+
+ self.treeview.grab_focus()
+
+ def _show_clicked_cb(self, widget, commit_sha1, parent_sha1, encoding):
+ """Callback for when the show button for a parent is clicked."""
+ window = DiffWindow()
+ window.set_diff(commit_sha1, parent_sha1, encoding)
+ self.treeview.grab_focus()
+
+without_diff = 0
+if __name__ == "__main__":
+
+ 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
index 0000000000..77c29de305
--- /dev/null
+++ b/contrib/gitview/gitview.txt
@@ -0,0 +1,56 @@
+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 gitlink:git-rev-list[1].
+
+Key Bindings
+------------
+F4::
+ To maximize the window
+
+F5::
+ To reread references.
+
+F11::
+ Full screen
+
+F12::
+ Leave full screen
+
+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
diff --git a/contrib/hg-to-git/hg-to-git.py b/contrib/hg-to-git/hg-to-git.py
new file mode 100755
index 0000000000..37337ff01f
--- /dev/null
+++ b/contrib/hg-to-git/hg-to-git.py
@@ -0,0 +1,233 @@
+#! /usr/bin/python
+
+""" hg-to-svn.py - A Mercurial to GIT converter
+
+ Copyright (C)2007 Stelian Pop <stelian@popies.net>
+
+ 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, 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+import os, os.path, sys
+import tempfile, popen2, pickle, getopt
+import re
+
+# Maps hg version -> git version
+hgvers = {}
+# List of children for each hg revision
+hgchildren = {}
+# Current branch for each hg revision
+hgbranch = {}
+
+#------------------------------------------------------------------------------
+
+def usage():
+
+ print """\
+%s: [OPTIONS] <hgprj>
+
+options:
+ -s, --gitstate=FILE: name of the state to be saved/read
+ for incrementals
+
+required:
+ hgprj: name of the HG project to import (directory)
+""" % sys.argv[0]
+
+#------------------------------------------------------------------------------
+
+def getgitenv(user, date):
+ env = ''
+ elems = re.compile('(.*?)\s+<(.*)>').match(user)
+ if elems:
+ env += 'export GIT_AUTHOR_NAME="%s" ;' % elems.group(1)
+ env += 'export GIT_COMMITER_NAME="%s" ;' % elems.group(1)
+ env += 'export GIT_AUTHOR_EMAIL="%s" ;' % elems.group(2)
+ env += 'export GIT_COMMITER_EMAIL="%s" ;' % elems.group(2)
+ else:
+ env += 'export GIT_AUTHOR_NAME="%s" ;' % user
+ env += 'export GIT_COMMITER_NAME="%s" ;' % user
+ env += 'export GIT_AUTHOR_EMAIL= ;'
+ env += 'export GIT_COMMITER_EMAIL= ;'
+
+ env += 'export GIT_AUTHOR_DATE="%s" ;' % date
+ env += 'export GIT_COMMITTER_DATE="%s" ;' % date
+ return env
+
+#------------------------------------------------------------------------------
+
+state = ''
+
+try:
+ opts, args = getopt.getopt(sys.argv[1:], 's:t:', ['gitstate=', 'tempdir='])
+ for o, a in opts:
+ if o in ('-s', '--gitstate'):
+ state = a
+ state = os.path.abspath(state)
+
+ if len(args) != 1:
+ raise('params')
+except:
+ usage()
+ sys.exit(1)
+
+hgprj = args[0]
+os.chdir(hgprj)
+
+if state:
+ if os.path.exists(state):
+ print 'State does exist, reading'
+ f = open(state, 'r')
+ hgvers = pickle.load(f)
+ else:
+ print 'State does not exist, first run'
+
+tip = os.popen('hg tip | head -1 | cut -f 2 -d :').read().strip()
+print 'tip is', tip
+
+# Calculate the branches
+print 'analysing the branches...'
+hgchildren["0"] = ()
+hgbranch["0"] = "master"
+for cset in range(1, int(tip) + 1):
+ hgchildren[str(cset)] = ()
+ prnts = os.popen('hg log -r %d | grep ^parent: | cut -f 2 -d :' % cset).readlines()
+ if len(prnts) > 0:
+ parent = prnts[0].strip()
+ else:
+ parent = str(cset - 1)
+ hgchildren[parent] += ( str(cset), )
+ if len(prnts) > 1:
+ mparent = prnts[1].strip()
+ hgchildren[mparent] += ( str(cset), )
+ else:
+ mparent = None
+
+ if mparent:
+ # For merge changesets, take either one, preferably the 'master' branch
+ if hgbranch[mparent] == 'master':
+ hgbranch[str(cset)] = 'master'
+ else:
+ hgbranch[str(cset)] = hgbranch[parent]
+ else:
+ # Normal changesets
+ # For first children, take the parent branch, for the others create a new branch
+ if hgchildren[parent][0] == str(cset):
+ hgbranch[str(cset)] = hgbranch[parent]
+ else:
+ hgbranch[str(cset)] = "branch-" + str(cset)
+
+if not hgvers.has_key("0"):
+ print 'creating repository'
+ os.system('git-init-db')
+
+# loop through every hg changeset
+for cset in range(int(tip) + 1):
+
+ # incremental, already seen
+ if hgvers.has_key(str(cset)):
+ continue
+
+ # get info
+ prnts = os.popen('hg log -r %d | grep ^parent: | cut -f 2 -d :' % cset).readlines()
+ if len(prnts) > 0:
+ parent = prnts[0].strip()
+ else:
+ parent = str(cset - 1)
+ if len(prnts) > 1:
+ mparent = prnts[1].strip()
+ else:
+ mparent = None
+
+ (fdcomment, filecomment) = tempfile.mkstemp()
+ csetcomment = os.popen('hg log -r %d -v | grep -v ^changeset: | grep -v ^parent: | grep -v ^user: | grep -v ^date | grep -v ^files: | grep -v ^description: | grep -v ^tag:' % cset).read().strip()
+ os.write(fdcomment, csetcomment)
+ os.close(fdcomment)
+
+ date = os.popen('hg log -r %d | grep ^date: | cut -f 2- -d :' % cset).read().strip()
+
+ tag = os.popen('hg log -r %d | grep ^tag: | cut -f 2- -d :' % cset).read().strip()
+
+ user = os.popen('hg log -r %d | grep ^user: | cut -f 2- -d :' % cset).read().strip()
+
+ print '-----------------------------------------'
+ print 'cset:', cset
+ print 'branch:', hgbranch[str(cset)]
+ print 'user:', user
+ print 'date:', date
+ print 'comment:', csetcomment
+ print 'parent:', parent
+ if mparent:
+ print 'mparent:', mparent
+ if tag:
+ print 'tag:', tag
+ print '-----------------------------------------'
+
+ # checkout the parent if necessary
+ if cset != 0:
+ if hgbranch[str(cset)] == "branch-" + str(cset):
+ print 'creating new branch', hgbranch[str(cset)]
+ os.system('git-checkout -b %s %s' % (hgbranch[str(cset)], hgvers[parent]))
+ else:
+ print 'checking out branch', hgbranch[str(cset)]
+ os.system('git-checkout %s' % hgbranch[str(cset)])
+
+ # merge
+ if mparent:
+ if hgbranch[parent] == hgbranch[str(cset)]:
+ otherbranch = hgbranch[mparent]
+ else:
+ otherbranch = hgbranch[parent]
+ print 'merging', otherbranch, 'into', hgbranch[str(cset)]
+ os.system(getgitenv(user, date) + 'git-merge --no-commit -s ours "" %s %s' % (hgbranch[str(cset)], otherbranch))
+
+ # remove everything except .git and .hg directories
+ os.system('find . \( -path "./.hg" -o -path "./.git" \) -prune -o ! -name "." -print | xargs rm -rf')
+
+ # repopulate with checkouted files
+ os.system('hg update -C %d' % cset)
+
+ # add new files
+ os.system('git-ls-files -x .hg --others | git-update-index --add --stdin')
+ # delete removed files
+ os.system('git-ls-files -x .hg --deleted | git-update-index --remove --stdin')
+
+ # commit
+ os.system(getgitenv(user, date) + 'git-commit -a -F %s' % filecomment)
+ os.unlink(filecomment)
+
+ # tag
+ if tag and tag != 'tip':
+ os.system(getgitenv(user, date) + 'git-tag %s' % tag)
+
+ # delete branch if not used anymore...
+ if mparent and len(hgchildren[str(cset)]):
+ print "Deleting unused branch:", otherbranch
+ os.system('git-branch -d %s' % otherbranch)
+
+ # retrieve and record the version
+ vvv = os.popen('git-show | head -1').read()
+ vvv = vvv[vvv.index(' ') + 1 : ].strip()
+ print 'record', cset, '->', vvv
+ hgvers[str(cset)] = vvv
+
+os.system('git-repack -a -d')
+
+# write the state for incrementals
+if state:
+ print 'Writing state'
+ f = open(state, 'w')
+ pickle.dump(hgvers, f)
+
+# vim: et ts=8 sw=4 sts=4
diff --git a/contrib/hg-to-git/hg-to-git.txt b/contrib/hg-to-git/hg-to-git.txt
new file mode 100644
index 0000000000..91f8fe6410
--- /dev/null
+++ b/contrib/hg-to-git/hg-to-git.txt
@@ -0,0 +1,21 @@
+hg-to-git.py is able to convert a Mercurial repository into a git one,
+and preserves the branches in the process (unlike tailor)
+
+hg-to-git.py can probably be greatly improved (it's a rather crude
+combination of shell and python) but it does already work quite well for
+me. Features:
+ - supports incremental conversion
+ (for keeping a git repo in sync with a hg one)
+ - supports hg branches
+ - converts hg tags
+
+Note that the git repository will be created 'in place' (at the same
+location as the source hg repo). You will have to manually remove the
+'.hg' directory after the conversion.
+
+Also note that the incremental conversion uses 'simple' hg changesets
+identifiers (ordinals, as opposed to SHA-1 ids), and since these ids
+are not stable across different repositories the hg-to-git.py state file
+is forever tied to one hg repository.
+
+Stelian Pop <stelian@popies.net>
diff --git a/contrib/hooks/post-receive-email b/contrib/hooks/post-receive-email
new file mode 100644
index 0000000000..65160153ee
--- /dev/null
+++ b/contrib/hooks/post-receive-email
@@ -0,0 +1,588 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Andy Parkins
+#
+# An example hook script to mail out commit update information. This hook sends emails
+# listing new revisions to the repository introduced by the change being reported. The
+# rule is that (for branch updates) each commit will appear on one email and one email
+# only.
+#
+# This hook is stored in the contrib/hooks directory. Your distribution will have put
+# this somewhere standard. You should make this script executable then link to it in
+# the repository you would like to use it in. For example, on debian the hook is stored
+# in /usr/share/doc/git-core/contrib/hooks/post-receive-email:
+#
+# chmod a+x post-receive-email
+# cd /path/to/your/repository.git
+# ln -sf /usr/share/doc/git-core/contrib/hooks/post-receive-email hooks/post-receive
+#
+# This hook script assumes it is enabled on the central repository of a project, with
+# all users pushing only to it and not between each other. It will still work if you
+# don't operate in that style, but it would become possible for the email to be from
+# someone other than the person doing the push.
+#
+# Config
+# ------
+# hooks.mailinglist
+# This is the list that all pushes will go to; leave it blank to not send
+# emails for every ref update.
+# hooks.announcelist
+# This is the list that all pushes of annotated tags will go to. Leave it
+# blank to default to the mailinglist field. The announce emails lists the
+# short log summary of the changes since the last annotated tag.
+# hook.envelopesender
+# If set then the -f option is passed to sendmail to allow the envelope sender
+# address to be set
+#
+# Notes
+# -----
+# All emails have their subjects prefixed with "[SCM]" to aid filtering.
+# All emails include the headers "X-Git-Refname", "X-Git-Oldrev",
+# "X-Git-Newrev", and "X-Git-Reftype" to enable fine tuned filtering and
+# give information for debugging.
+#
+
+# ---------------------------- Functions
+
+#
+# Top level email generation function. This decides what type of update
+# this is and calls the appropriate body-generation routine after outputting
+# the common header
+#
+# Note this function doesn't actually generate any email output, that is taken
+# care of by the functions it calls:
+# - generate_email_header
+# - generate_create_XXXX_email
+# - generate_update_XXXX_email
+# - generate_delete_XXXX_email
+# - generate_email_footer
+#
+generate_email()
+{
+ # --- Arguments
+ oldrev=$(git rev-parse $1)
+ newrev=$(git rev-parse $2)
+ refname="$3"
+
+ # --- Interpret
+ # 0000->1234 (create)
+ # 1234->2345 (update)
+ # 2345->0000 (delete)
+ if expr "$oldrev" : '0*$' >/dev/null
+ then
+ change_type="create"
+ else
+ if expr "$newrev" : '0*$' >/dev/null
+ then
+ change_type="delete"
+ else
+ change_type="update"
+ fi
+ fi
+
+ # --- Get the revision types
+ newrev_type=$(git cat-file -t $newrev 2> /dev/null)
+ oldrev_type=$(git cat-file -t "$oldrev" 2> /dev/null)
+ case "$change_type" in
+ create|update)
+ rev="$newrev"
+ rev_type="$newrev_type"
+ ;;
+ delete)
+ rev="$oldrev"
+ rev_type="$oldrev_type"
+ ;;
+ esac
+
+ # The revision type tells us what type the commit is, combined with
+ # the location of the ref we can decide between
+ # - working branch
+ # - tracking branch
+ # - unannoted tag
+ # - annotated tag
+ case "$refname","$rev_type" in
+ refs/tags/*,commit)
+ # un-annotated tag
+ refname_type="tag"
+ short_refname=${refname##refs/tags/}
+ ;;
+ refs/tags/*,tag)
+ # annotated tag
+ refname_type="annotated tag"
+ short_refname=${refname##refs/tags/}
+ # change recipients
+ if [ -n "$announcerecipients" ]; then
+ recipients="$announcerecipients"
+ fi
+ ;;
+ refs/heads/*,commit)
+ # branch
+ refname_type="branch"
+ short_refname=${refname##refs/heads/}
+ ;;
+ refs/remotes/*,commit)
+ # tracking branch
+ refname_type="tracking branch"
+ short_refname=${refname##refs/remotes/}
+ echo >&2 "*** Push-update of tracking branch, $refname"
+ echo >&2 "*** - no email generated."
+ exit 0
+ ;;
+ *)
+ # Anything else (is there anything else?)
+ echo >&2 "*** Unknown type of update to $refname ($rev_type)"
+ echo >&2 "*** - no email generated"
+ exit 1
+ ;;
+ esac
+
+ # Check if we've got anyone to send to
+ if [ -z "$recipients" ]; then
+ echo >&2 "*** hooks.recipients is not set so no email will be sent"
+ echo >&2 "*** for $refname update $oldrev->$newrev"
+ exit 0
+ fi
+
+ # Email parameters
+ # The committer will be obtained from the latest existing rev; so
+ # for a deletion it will be the oldrev, for the others, then newrev
+ committer=$(git show --pretty=full -s $rev | sed -ne "s/^Commit: //p" |
+ sed -ne 's/\(.*\) </"\1" </p')
+ # The email subject will contain the best description of the ref
+ # that we can build from the parameters
+ describe=$(git describe $rev 2>/dev/null)
+ if [ -z "$describe" ]; then
+ describe=$rev
+ fi
+
+ generate_email_header
+
+ # Call the correct body generation function
+ fn_name=general
+ case "$refname_type" in
+ "tracking branch"|branch)
+ fn_name=branch
+ ;;
+ "annotated tag")
+ fn_name=atag
+ ;;
+ esac
+ generate_${change_type}_${fn_name}_email
+
+ generate_email_footer
+}
+
+generate_email_header()
+{
+ # --- Email (all stdout will be the email)
+ # Generate header
+ cat <<-EOF
+ From: $committer
+ To: $recipients
+ Subject: ${EMAILPREFIX}$projectdesc $refname_type, $short_refname, ${change_type}d. $describe
+ X-Git-Refname: $refname
+ X-Git-Reftype: $refname_type
+ X-Git-Oldrev: $oldrev
+ X-Git-Newrev: $newrev
+
+ This is an automated email from the git hooks/post-receive script. It was
+ generated because a ref change was pushed to the repository containing
+ the project "$projectdesc".
+
+ The $refname_type, $short_refname has been ${change_type}d
+ EOF
+}
+
+generate_email_footer()
+{
+ cat <<-EOF
+
+
+ hooks/post-receive
+ --
+ $projectdesc
+ EOF
+}
+
+# --------------- Branches
+
+#
+# Called for the creation of a branch
+#
+generate_create_branch_email()
+{
+ # This is a new branch and so oldrev is not valid
+ echo " at $newrev ($newrev_type)"
+ echo ""
+
+ echo $LOGBEGIN
+ # This shows all log entries that are not already covered by
+ # another ref - i.e. commits that are now accessible from this
+ # ref that were previously not accessible (see generate_update_branch_email
+ # for the explanation of this command)
+ git rev-parse --not --branches | grep -v $(git rev-parse $refname) |
+ git rev-list --pretty --stdin $newrev
+ echo $LOGEND
+}
+
+#
+# Called for the change of a pre-existing branch
+#
+generate_update_branch_email()
+{
+ # Consider this:
+ # 1 --- 2 --- O --- X --- 3 --- 4 --- N
+ #
+ # O is $oldrev for $refname
+ # N is $newrev for $refname
+ # X is a revision pointed to by some other ref, for which we may
+ # assume that an email has already been generated.
+ # In this case we want to issue an email containing only revisions
+ # 3, 4, and N. Given (almost) by
+ #
+ # git-rev-list N ^O --not --all
+ #
+ # The reason for the "almost", is that the "--not --all" will take
+ # precedence over the "N", and effectively will translate to
+ #
+ # git-rev-list N ^O ^X ^N
+ #
+ # So, we need to build up the list more carefully. git-rev-parse will
+ # generate a list of revs that may be fed into git-rev-list. We can get
+ # it to make the "--not --all" part and then filter out the "^N" with:
+ #
+ # git-rev-parse --not --all | grep -v N
+ #
+ # Then, using the --stdin switch to git-rev-list we have effectively
+ # manufactured
+ #
+ # git-rev-list N ^O ^X
+ #
+ # This leaves a problem when someone else updates the repository
+ # while this script is running. Their new value of the ref we're working
+ # on would be included in the "--not --all" output; and as our $newrev
+ # would be an ancestor of that commit, it would exclude all of our
+ # commits. What we really want is to exclude the current value of
+ # $refname from the --not list, rather than N itself. So:
+ #
+ # git-rev-parse --not --all | grep -v $(git-rev-parse $refname)
+ #
+ # Get's us to something pretty safe (apart from the small time between
+ # refname being read, and git-rev-parse running - for that, I give up)
+ #
+ #
+ # Next problem, consider this:
+ # * --- B --- * --- O ($oldrev)
+ # \
+ # * --- X --- * --- N ($newrev)
+ #
+ # That is to say, there is no guarantee that oldrev is a strict subset of
+ # newrev (it would have required a --force, but that's allowed). So, we
+ # can't simply say rev-list $oldrev..$newrev. Instead we find the common
+ # base of the two revs and list from there.
+ #
+ # As above, we need to take into account the presence of X; if another
+ # branch is already in the repository and points at some of the revisions
+ # that we are about to output - we don't want them. The solution is as
+ # before: git-rev-parse output filtered.
+ #
+ # Finally, tags:
+ # 1 --- 2 --- O --- T --- 3 --- 4 --- N
+ #
+ # Tags pushed into the repository generate nice shortlog emails that
+ # summarise the commits between them and the previous tag. However,
+ # those emails don't include the full commit messages that we output
+ # for a branch update. Therefore we still want to output revisions
+ # that have been output on a tag email.
+ #
+ # Luckily, git-rev-parse includes just the tool. Instead of using "--all"
+ # we use "--branches"; this has the added benefit that "remotes/" will
+ # be ignored as well.
+
+ # List all of the revisions that were removed by this update, in a fast forward
+ # update, this list will be empty, because rev-list O ^N is empty. For a non
+ # fast forward, O ^N is the list of removed revisions
+ fastforward=""
+ rev=""
+ for rev in $(git rev-list $newrev..$oldrev)
+ do
+ revtype=$(git cat-file -t "$rev")
+ echo " discards $rev ($revtype)"
+ done
+ if [ -z "$rev" ]; then
+ fast_forward=1
+ fi
+
+ # List all the revisions from baserev to newrev in a kind of
+ # "table-of-contents"; note this list can include revisions that have
+ # already had notification emails and is present to show the full detail
+ # of the change from rolling back the old revision to the base revision and
+ # then forward to the new revision
+ for rev in $(git rev-list $oldrev..$newrev)
+ do
+ revtype=$(git cat-file -t "$rev")
+ echo " via $rev ($revtype)"
+ done
+
+ if [ -z "$fastforward" ]; then
+ echo " from $oldrev ($oldrev_type)"
+ else
+ echo ""
+ echo "This update added new revisions after undoing old revisions. That is to"
+ echo "say, the old revision is not a strict subset of the new revision. This"
+ echo "situation occurs when you --force push a change and generate a"
+ echo "repository containing something like this:"
+ echo ""
+ echo " * -- * -- B -- O -- O -- O ($oldrev)"
+ echo " \\"
+ echo " N -- N -- N ($newrev)"
+ echo ""
+ echo "When this happens we assume that you've already had alert emails for all"
+ echo "of the O revisions, and so we here report only the revisions in the N"
+ echo "branch from the common base, B."
+ fi
+
+ echo ""
+ echo "Those revisions listed above that are new to this repository have"
+ echo "not appeared on any other notification email; so we list those"
+ echo "revisions in full, below."
+
+ echo ""
+ echo $LOGBEGIN
+ git rev-parse --not --branches | grep -v $(git rev-parse $refname) |
+ git rev-list --pretty --stdin $oldrev..$newrev
+
+ # XXX: Need a way of detecting whether git rev-list actually outputted
+ # anything, so that we can issue a "no new revisions added by this
+ # update" message
+
+ echo $LOGEND
+
+ # The diffstat is shown from the old revision to the new revision. This
+ # is to show the truth of what happened in this change. There's no point
+ # showing the stat from the base to the new revision because the base
+ # is effectively a random revision at this point - the user will be
+ # interested in what this revision changed - including the undoing of
+ # previous revisions in the case of non-fast forward updates.
+ echo ""
+ echo "Summary of changes:"
+ git diff-tree --stat --summary --find-copies-harder $oldrev..$newrev
+}
+
+#
+# Called for the deletion of a branch
+#
+generate_delete_branch_email()
+{
+ echo " was $oldrev"
+ echo ""
+ echo $LOGEND
+ git show -s --pretty=oneline $oldrev
+ echo $LOGEND
+}
+
+# --------------- Annotated tags
+
+#
+# Called for the creation of an annotated tag
+#
+generate_create_atag_email()
+{
+ echo " at $newrev ($newrev_type)"
+
+ generate_atag_email
+}
+
+#
+# Called for the update of an annotated tag (this is probably a rare event
+# and may not even be allowed)
+#
+generate_update_atag_email()
+{
+ echo " to $newrev ($newrev_type)"
+ echo " from $oldrev (which is now obsolete)"
+
+ generate_atag_email
+}
+
+#
+# Called when an annotated tag is created or changed
+#
+generate_atag_email()
+{
+ # Use git-for-each-ref to pull out the individual fields from the tag
+ eval $(git for-each-ref --shell --format='
+ tagobject=%(*objectname)
+ tagtype=%(*objecttype)
+ tagger=%(taggername)
+ tagged=%(taggerdate)' $refname
+ )
+
+ echo " tagging $tagobject ($tagtype)"
+ case "$tagtype" in
+ commit)
+ # If the tagged object is a commit, then we assume this is a
+ # release, and so we calculate which tag this tag is replacing
+ prevtag=$(git describe --abbrev=0 $newrev^ 2>/dev/null)
+
+ if [ -n "$prevtag" ]; then
+ echo " replaces $prevtag"
+ fi
+ ;;
+ *)
+ echo " length $(git cat-file -s $tagobject) bytes"
+ ;;
+ esac
+ echo " tagged by $tagger"
+ echo " on $tagged"
+
+ echo ""
+ echo $LOGBEGIN
+
+ # Show the content of the tag message; this might contain a change log
+ # or release notes so is worth displaying.
+ git cat-file tag $newrev | sed -e '1,/^$/d'
+
+ echo ""
+ case "$tagtype" in
+ commit)
+ # Only commit tags make sense to have rev-list operations performed
+ # on them
+ if [ -n "$prevtag" ]; then
+ # Show changes since the previous release
+ git rev-list --pretty=short "$prevtag..$newrev" | git shortlog
+ else
+ # No previous tag, show all the changes since time began
+ git rev-list --pretty=short $newrev | git shortlog
+ fi
+ ;;
+ *)
+ # XXX: Is there anything useful we can do for non-commit objects?
+ ;;
+ esac
+
+ echo $LOGEND
+}
+
+#
+# Called for the deletion of an annotated tag
+#
+generate_delete_atag_email()
+{
+ echo " was $oldrev"
+ echo ""
+ echo $LOGEND
+ git show -s --pretty=oneline $oldrev
+ echo $LOGEND
+}
+
+# --------------- General references
+
+#
+# Called when any other type of reference is created (most likely a
+# non-annotated tag)
+#
+generate_create_general_email()
+{
+ echo " at $newrev ($newrev_type)"
+
+ generate_general_email
+}
+
+#
+# Called when any other type of reference is updated (most likely a
+# non-annotated tag)
+#
+generate_update_general_email()
+{
+ echo " to $newrev ($newrev_type)"
+ echo " from $oldrev"
+
+ generate_general_email
+}
+
+#
+# Called for creation or update of any other type of reference
+#
+generate_general_email()
+{
+ # Unannotated tags are more about marking a point than releasing a version;
+ # therefore we don't do the shortlog summary that we do for annotated tags
+ # above - we simply show that the point has been marked, and print the log
+ # message for the marked point for reference purposes
+ #
+ # Note this section also catches any other reference type (although there
+ # aren't any) and deals with them in the same way.
+
+ echo ""
+ if [ "$newrev_type" = "commit" ]; then
+ echo $LOGBEGIN
+ git show --no-color --root -s $newrev
+ echo $LOGEND
+ else
+ # What can we do here? The tag marks an object that is not a commit,
+ # so there is no log for us to display. It's probably not wise to
+ # output git-cat-file as it could be a binary blob. We'll just say how
+ # big it is
+ echo "$newrev is a $newrev_type, and is $(git cat-file -s $newrev) bytes long."
+ fi
+}
+
+#
+# Called for the deletion of any other type of reference
+#
+generate_delete_general_email()
+{
+ echo " was $oldrev"
+ echo ""
+ echo $LOGEND
+ git show -s --pretty=oneline $oldrev
+ echo $LOGEND
+}
+
+# ---------------------------- main()
+
+# --- Constants
+EMAILPREFIX="[SCM] "
+LOGBEGIN="- Log -----------------------------------------------------------------"
+LOGEND="-----------------------------------------------------------------------"
+
+# --- Config
+# Set GIT_DIR either from the working directory, or from the environment
+# variable.
+GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
+if [ -z "$GIT_DIR" ]; then
+ echo >&2 "fatal: post-receive: GIT_DIR not set"
+ exit 1
+fi
+
+projectdesc=$(sed -e '1p' "$GIT_DIR/description")
+# Check if the description is unchanged from it's default, and shorten it to a
+# more manageable length if it is
+if expr "$projectdesc" : "Unnamed repository.*$" >/dev/null
+then
+ projectdesc="UNNAMED PROJECT"
+fi
+
+recipients=$(git repo-config hooks.mailinglist)
+announcerecipients=$(git repo-config hooks.announcelist)
+envelopesender=$(git-repo-config hooks.envelopesender)
+
+# --- Main loop
+# Allow dual mode: run from the command line just like the update hook, or if
+# no arguments are given then run as a hook script
+if [ -n "$1" -a -n "$2" -a -n "$3" ]; then
+ # Output to the terminal in command line mode - if someone wanted to
+ # resend an email; they could redirect the output to sendmail themselves
+ PAGER= generate_email $2 $3 $1
+else
+ if [ -n "$envelopesender" ]; then
+ envelopesender="-f '$envelopesender'"
+ fi
+
+ while read oldrev newrev refname
+ do
+ generate_email $oldrev $newrev $refname |
+ /usr/sbin/sendmail -t $envelopesender
+ done
+fi
diff --git a/contrib/remotes2config.sh b/contrib/remotes2config.sh
new file mode 100644
index 0000000000..dc09eae972
--- /dev/null
+++ b/contrib/remotes2config.sh
@@ -0,0 +1,35 @@
+#!/bin/sh
+
+# Use this tool to rewrite your .git/remotes/ files into the config.
+
+. git-sh-setup
+
+if [ -d "$GIT_DIR"/remotes ]; then
+ echo "Rewriting $GIT_DIR/remotes" >&2
+ error=0
+ # rewrite into config
+ {
+ cd "$GIT_DIR"/remotes
+ ls | while read f; do
+ name=$(printf "$f" | tr -c "A-Za-z0-9" ".")
+ sed -n \
+ -e "s/^URL: \(.*\)$/remote.$name.url \1 ./p" \
+ -e "s/^Pull: \(.*\)$/remote.$name.fetch \1 ^$ /p" \
+ -e "s/^Push: \(.*\)$/remote.$name.push \1 ^$ /p" \
+ < "$f"
+ done
+ echo done
+ } | while read key value regex; do
+ case $key in
+ done)
+ if [ $error = 0 ]; then
+ mv "$GIT_DIR"/remotes "$GIT_DIR"/remotes.old
+ fi ;;
+ *)
+ echo "git-config $key "$value" $regex"
+ git-config $key "$value" $regex || error=1 ;;
+ esac
+ done
+fi
+
+
diff --git a/contrib/vim/README b/contrib/vim/README
new file mode 100644
index 0000000000..9e7881fea9
--- /dev/null
+++ b/contrib/vim/README
@@ -0,0 +1,8 @@
+To syntax highlight git's commit messages, you need to:
+ 1. Copy syntax/gitcommit.vim to vim's syntax directory:
+ $ mkdir -p $HOME/.vim/syntax
+ $ cp syntax/gitcommit.vim $HOME/.vim/syntax
+ 2. Auto-detect the editing of git commit files:
+ $ cat >>$HOME/.vimrc <<'EOF'
+ autocmd BufNewFile,BufRead COMMIT_EDITMSG set filetype=gitcommit
+ EOF
diff --git a/contrib/vim/syntax/gitcommit.vim b/contrib/vim/syntax/gitcommit.vim
new file mode 100644
index 0000000000..332121b40e
--- /dev/null
+++ b/contrib/vim/syntax/gitcommit.vim
@@ -0,0 +1,18 @@
+syn region gitLine start=/^#/ end=/$/
+syn region gitCommit start=/^# Changes to be committed:$/ end=/^#$/ contains=gitHead,gitCommitFile
+syn region gitHead contained start=/^# (.*)/ end=/^#$/
+syn region gitChanged start=/^# Changed but not updated:/ end=/^#$/ contains=gitHead,gitChangedFile
+syn region gitUntracked start=/^# Untracked files:/ end=/^#$/ contains=gitHead,gitUntrackedFile
+
+syn match gitCommitFile contained /^#\t.*/hs=s+2
+syn match gitChangedFile contained /^#\t.*/hs=s+2
+syn match gitUntrackedFile contained /^#\t.*/hs=s+2
+
+hi def link gitLine Comment
+hi def link gitCommit Comment
+hi def link gitChanged Comment
+hi def link gitHead Comment
+hi def link gitUntracked Comment
+hi def link gitCommitFile Type
+hi def link gitChangedFile Constant
+hi def link gitUntrackedFile Constant
diff --git a/contrib/workdir/git-new-workdir b/contrib/workdir/git-new-workdir
new file mode 100755
index 0000000000..9877b98508
--- /dev/null
+++ b/contrib/workdir/git-new-workdir
@@ -0,0 +1,57 @@
+#!/bin/sh
+
+usage () {
+ echo "usage:" $@
+ exit 127
+}
+
+die () {
+ echo $@
+ exit 128
+}
+
+if test $# -lt 2 || test $# -gt 3
+then
+ usage "$0 <repository> <new_workdir> [<branch>]"
+fi
+
+orig_git=$1
+new_workdir=$2
+branch=$3
+
+# want to make sure that what is pointed to has a .git directory ...
+test -d "$orig_git/.git" || die "\"$orig_git\" is not a git repository!"
+
+# don't link to a workdir
+if test -L "$orig_git/.git/config"
+then
+ die "\"$orig_git\" is a working directory only, please specify" \
+ "a complete repository."
+fi
+
+# make sure the the links use full paths
+orig_git=$(cd "$orig_git"; pwd)
+
+# create the workdir
+mkdir -p "$new_workdir/.git" || die "unable to create \"$new_workdir\"!"
+
+# create the links to the original repo. explictly exclude index, HEAD and
+# logs/HEAD from the list since they are purely related to the current working
+# directory, and should not be shared.
+for x in config refs logs/refs objects info hooks packed-refs remotes rr-cache
+do
+ case $x in
+ */*)
+ mkdir -p "$(dirname "$new_workdir/.git/$x")"
+ ;;
+ esac
+ ln -s "$orig_git/.git/$x" "$new_workdir/.git/$x"
+done
+
+# now setup the workdir
+cd "$new_workdir"
+# copy the HEAD from the original repository as a default branch
+cp "$orig_git/.git/HEAD" .git/HEAD
+# checkout the branch (either the same as HEAD from the original repository, or
+# the one that was asked for)
+git checkout -f $branch